注册
环信即时通讯云

环信即时通讯云

单聊、群聊、聊天室...
环信开发文档

环信开发文档

环信FAQ

环信FAQ

集成常见问题及答案
RTE开发者社区

RTE开发者社区

汇聚音视频领域技术干货,分享行业资讯
技术讨论区

技术讨论区

技术交流、答疑
资源下载

资源下载

收集了海量宝藏开发资源
iOS Library

iOS Library

不需要辛辛苦苦的去找轮子, 这里都有
Android Library

Android Library

不需要辛辛苦苦的去找轮子, 这里都有

开源项目|使用go语言搭建高效的环信 IM Rest接口(附源码)

项目背景环信 Server SDK 是对环信 IM REST API 的封装, 可以节省服务器端开发者对接环信 API 的时间,只需要配置自己的 App Key 相关信息即可使用。环信目前提供java和PHP版本的Server SDK,此项目使用go语言对环信...
继续阅读 »

项目背景

环信 Server SDK 是对环信 IM REST API 的封装, 可以节省服务器端开发者对接环信 API 的时间,只需要配置自己的 App Key 相关信息即可使用。

环信目前提供java和PHP版本的Server SDK,此项目使用go语言对环信 IM REST API 进行封装,对官方版本进行了补充,有需要的开发者可以直接通过以下地址获取源码。

项目地址

前提条件

  • go语言环境

  • 有效的环信即时通讯 IM 开发者账号和 AppKey、ClientID、ClientSecret、DomainURL
    登录 环信管理后台 到“应用列表” → 点击“查看”即可获取到 App Key、Client ID、ClientSecret,到"即时通讯" → 点击"服务概览"获取到 “Rest api” 的服务器域名。

实现方法

  • go.mod 文件引入: github.com/xiaofengin/easemob-go

AppKey、ClientID、ClientSecret在下图中获取


DomainURL在下图中获取

初始化IM SDK

package main

import (
"context"
"fmt"
IMSDK "github.com/xiaofengin/easemob-go"
)

func main() {
client, err := IMSDK.New("appkey",
"clientId",
"clientSecret",
"domainURL")
if err != nil {
return
}
}

批量注册两个用户

package main

import (
"context"
"fmt"
IMSDK "github.com/xiaofengin/easemob-go"
)

func main() {
client, err := IMSDK.New("appkey",
"clientId",
"clientSecret",
"domainURL")
if err != nil {
return
}
user1 := UserRegisterParam{
Username: "userID_1",
Password: "1",
}
user2 := UserRegisterParam{
Username: "userID_2",
Password: "1",
}
users := []UserRegisterParam{user1, user2}
ret, err := client.UserRegister(context.Background(), &users)
if err != nil {
return
}
fmt.Printf("数据的值:%v\n", ret.Entities)
}

发送一个单聊消息

  • tos 放接收方环信ID(多个)m := CreateTextMsg("hello word", tos) 创建一个消息体
  • 默认发送方ID 是 admin,如果要修改的话 m.From = "指定ID"
  • 也可以给消息添加扩展字段 m.Ext = map[string]interface{}{"key1": "value1", "key2": "value2"}
package main

import (
"context"
"fmt"
IMSDK "github.com/xiaofengin/easemob-go"
)

func main() {
client, err := IMSDK.New("appkey",
"clientId",
"clientSecret",
"domainURL")
if err != nil {
return
}
var tos []string
tos = append(tos, "环信用户ID")
m := CreateTextMsg("hello word", tos)
//m.From = "指定ID"
//m.Ext = map[string]interface{}{"key1": "value1", "key2": "value2"}
ret, err := client.SendChatMessage(context.Background(), m)
if err != nil {
return
}
fmt.Printf("数据的值:%v\n", ret.Data)
}

获取用户token

  • 通过用户ID和密码获取用户token,也可以通过用户ID获取用户token
package main

import (
"context"
"fmt"
IMSDK "github.com/xiaofengin/easemob-go"
)

func main() {
client, err := IMSDK.New("appkey",
"clientId",
"clientSecret",
"domainURL")
if err != nil {
return
}

//通过用户 ID 和密码获取用户 token
//data := TokenParam{
// GrantType: "password",
// Username: "userID",
// Password: "1",
// Ttl: "1024000",
//}

//通过用户 ID 获取用户 token
data := TokenParam{
GrantType: "inherit",
Username: "userID",
AutoCreateUser: true,
Ttl: "1024000",
}
ret, err := client.GetUserToken(context.Background(), &data)
if err != nil {
return
}
fmt.Printf("数据的值:%v\n", ret.AccessToken)
}

SDK功能清单

注意

测试代码中 appkey clientId clientSecret 这三个参数我是写到环境变量里面,
如果 你没有把参数写到环境变量里面,可以直接写死该参数


参考文档:

IMGeek社区支持:https://www.imgeek.net

收起阅读 »

【附源码】推荐8个「IM+AI」场景的开源项目,建议收藏

1、社交泛娱乐——【找搭子】饭搭子、旅游搭子、遛狗搭子…这种新型的“搭子社交”在年轻群体中逐渐流行起来。区别于传统社交,搭子关系,陌生以上,友人未满,这种轻松的浅社交既能获得志趣相投的陪伴,又不用苦心经营彼此的关系。实在是好搭子不问出处,只要有能结伴做的事,就...
继续阅读 »

1、社交泛娱乐——【找搭子】

饭搭子、旅游搭子、遛狗搭子…这种新型的“搭子社交”在年轻群体中逐渐流行起来。区别于传统社交,搭子关系,陌生以上,友人未满,这种轻松的浅社交既能获得志趣相投的陪伴,又不用苦心经营彼此的关系。实在是

好搭子不问出处,只要有能结伴做的事,就可以有那么一“搭”。那么问题来了,如何精准地匹配到自己喜爱的活动以及志趣相投的“搭子”,便成了求搭者们的刚需。

项目介绍

“找搭子”——基于AI Agent解决找搭子最后一公里。它借助环信IM能力,使用对话Agent 充分挖掘用户需求,整合用户信息形成实时活动表单,精准推荐,打破固化标签,增加可信度。为活动发起者、参与者提供了一个双向智能、高效沟通、精准匹配的平台。


(找搭子产品架构)

(找搭子Demo演示)

项目点评:

“找搭子”以其独特的创新视角,展现了信息分发和成员组织的全新可能性。通过利用大模型进行精准匹配,该项目能够根据用户的兴趣爱好,智能地形成特定的用户群体。这种创新的方式不仅极大地提高了用户的参与度和满意度,同时也预示着巨大的商业潜力。在营销、社交和社区建设等领域,其影响力不可忽视。"找搭子"的实现,无疑是对传统模式的一次颠覆性的创新,它为我们打开了一个全新的视角,重新诠释了我们对于用户互动和社区建设的理解。

模型:环信IM+文心一言

项目源码:(Flutter)

2、教育培训——KidChat

项目介绍

「KidChat」是一个儿童故事创作分享社区。通过语音输入故事梗概,由 LLM 生成儿童故事,结合环信IM能力,自动将绘本故事分享到故事广场,让小朋友们一起欣赏。通过 Reaction 互动,并自动为每一个故事生成子区,可在子区内对具体故事进一步讨论,进而形成一个有趣而充满创造力的故事分享社区。

kidchat Demo演示

项目点评

「KidChat」以其卓越的内容生成和讲述能力,成功地满足了面向低龄孩子的“讲故事”场景的需求。该项目利用大模型的强大功能,能够快速生成丰富多样、充满无限可能的故事,并通过语音进行生动有趣的讲解。这无疑满足了低龄儿童家长的刚需,为他们提供了一种新的、高效的儿童教育方式。在实现过程中,KidChat非常富有创意地将环信的Thread和Reaction功能应用到了自己的业务功能实现中,提供了非常好的使用体验。更值得一提的是,该项目的完成度高,用户体验佳,无论是在故事内容的创新性,还是在语音讲解的生动性上,都表现出了极高的专业水准和创新思维。

模型:环信IM+讯飞星火

项目源码:(Flutter)

3、聊天机器人——ChattyAI

项目介绍

「ChattyAI」是一款基于人工智能技术的智能陪聊机器人,旨在提供用户个性化、有趣、愉快的对话体验。通过先进的自然语言处理和机器学习算法,ChattyAI能够理解用户的意图和情感,并以富有情感的方式回应,为用户提供真实感和沟通舒适度。

为增加智能体与用户的互动体验与亲密感,借助环信IM通讯能力,通过智能体向用户定时发送早安、午安、晚安。除此外还可以扩展更丰富多样的主动唤起话题的任务。



项目点评

「ChattyAI」在社交泛娱乐领域实现了人与智能体聊天的全新场景。该项目充分利用大模型在闲聊领域的优势,为用户提供了一种全新的、高质量的交流体验。"ChattyAI"不仅提供了丰富人设的虚拟角色,更在UI设计和交互体验上展现出了卓越的水准。其UI设计优雅且直观,交互体验流畅且自然,使得用户可以轻松地与虚拟角色进行交流。项目的完成度非常高,无论是在技术实现,还是在用户体验上,都展现出了项目团队的专业能力和创新思维。"ChattyAI"的成功实现,为我们在社交泛娱乐领域的探索提供了有力的启示。

模型:环信IM+MINIMAX

项目源码:(Android)

4、AI专家助手

现存市面上大部分的AI主要是面向个人的,而当前的AI助手并不能准确无误的解决大部分人的问题,尤其在一些特定的领域,比如编程,医疗等专业技能要求较高的方向,虽然AI能够给出相应的回复,但是这些回复对于普通人来说,甄别其中的准确性依然存在一定问题。该项目通过一般咨询者的信息,收集不同AI厂商的建议或者帮助信息,能够大大提升相应的工作效率。进而实现专家的效率,而当前社会专家的稀缺才是更大的瓶颈。

模型:环信IM+百川智能+MINIMAX+文心一言

项目源码:

5、ai群管家

此项目提供了群聊、单聊机器人对话服务。通过命令方式激活机器人,单聊bot可实现与多个角色如AI医生、知乎文案生成、AI家教、AI律师、地方美食推荐(自由切换)生成对话,群聊借助AI能力提供了群历史消息总结功能。是超级社群中典型的应用场景。

模型:环信IM+智谱AI

项目源码:

6、PictoChat (AI智绘)

AI智绘是一款创新的移动应用程序(iOS),整合了环信IM(即时通讯)平台和OpenAI的GPT-4.0绘图能力。这款应用专为提升在线交流体验而设计,能够在实时对话中生成和发送图片,极大地增强了交流的趣味性和互动性。

模型:环信IM+ChatGPT

项目源码:

7、百答

百答,一个All in One全能助手,你的AI智囊团。基于环信IM即时通讯解决方案,结合各家大模型能力开发的全能AI助手。互联网+环信IM+大模型,强强联合,多重BUFF叠加,让普通人也拥有撬动地球的力量!恋爱大师,编程助手,周报秘书,抬杠大师,彩虹屁专家,数字女友,礼物攻略等等应有尽有,全能智能 有问必答,堪称bot细分领域第一代卷王。

模型:环信IM+通义千问

项目源码:

8、智慧医疗

此项目是一款智慧医疗应用,患者与AI医生实时互动获得实时的医疗咨询,会话结束时为患者生成咨询档案,同时智能推荐相应的科室。当患者问诊相应科室医生时,医生可调取患者基本信息和与AI医生的历史咨询档案。

智慧医疗的蓬勃发展有望提升整个医疗体系的质量和效率,改善医疗体验,同时为患者提供更为个性化和便捷的医疗服务。我们期待在医疗领域,IM与AI的紧密融合发挥巨大的作用。

模型:环信IM+通义千问

项目源码:

参考资料:

环信集成相关

收起阅读 »

程序员创业:从技术到商业的转变

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。 为什么程...
继续阅读 »

作为一名程序员,我们通常会聚焦于编程技能和技术能力的提升,这也是我们日常工作的主要职责。但是,随着技术的不断发展和市场的变化,仅仅依靠技术能力已经不足以支撑我们在职场上的发展和求职竞争力了。所以,作为一名有远大理想的程序员,我们应该考虑创业的可能性。


为什么程序员要创业?


创业其实并非只适用于商学院的毕业生或者有创新理念的企业家。程序员在业内有着相当高的技术储备和市场先知,因此更容易从技术角度前瞻和切入新兴市场,更好地利用技术储备来实现创业梦想。


此外,创业可以释放我们的潜力,同时也可以让我们找到自己的定位和方向。在创业的过程中,我们可能会遇到各种挑战和困难,但这些挑战也将锻炼我们的意志力和决策能力,让我们更好地发挥自己的潜力。


创业需要具备的技能


作为一名技术人员,创业需要具备更多的技能。首先是商业和运营的技能:包括市场分析、用户研究、产品策划、项目管理等。其次是团队管理和沟通能力,在创业的过程中,人才的招聘和管理是核心问题。


另外,还需要具备跨界合作的能力,通过开放性的合作与交流,借助不同团队的技术和资源,完成创业项目。所以我们应该将跨界合作看作是创业过程中的重要选择,选择和加强自己的跨界交流和合作能力,也能为我们的企业注入活力和创新精神。


如何创业?


从技术到商业的转变,从最初想法的诞生到成熟的企业的创立,都需要一个创业的路线图。以下是一些需要注意的事项:




  1. 研究市场:了解市场趋势,分析需求,制定产品策略。可以去参加行业论坛,争取到专业意见和帮助。




  2. 制定商业计划:包括产品方案、市场营销、项目管理、团队建设等。制定一个系统的商业计划是投资者和团队成员对创业企业的认可。




  3. 招募团队:由于我们一般不是经验丰富的企业家,团队的选择尤为重要。要找的不仅要是技能和经验匹配的团队,更要找能一起携手完成创业项目的合作者。




  4. 行动计划:从实现规划步入到实战行动是创业项目的关键。按部就班地完成阶段性任务,控制实施进度和途中变化,在完成一个阶段后可以重新评估计划。




  5. 完成任务并分析:最后,团队成员需要根据企业进展,完整阶段性的目标,做自己的工作。及时完成考核任务并一起分享数据分析、事件解决和项目总结等信息,为项目下一阶段做出准确预测。




结语


创业是一条充满挑战性和机遇的路线,也是在我们的技术和业务的进一步升级中一条非常良好的通道。越来越多的技术人员意识到了自己的潜力,开始考虑自己创业的可能性。只要学会逐步掌握创业所需的技能和知识,并制订出详细的创业路线图,大可放手去尝试,才能最终实现自己心中的创业梦想。


作者:郝学胜
链接:https://juejin.cn/post/7240465997002047547
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(下)

上篇:https://www.imgeek.net/article/825362923声网RTC接入, 直播与语音实现接入在views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, ...
继续阅读 »

上篇:https://www.imgeek.net/article/825362923

声网RTC接入, 直播与语音实现

接入

views/Channel/components文件夹下新增一个组件StreamHandler, 该组件为后续我们处理游戏房间的组件, 先初步编写声网接入逻辑

// views/Channel/components/StreamHandler/index.js

const options = {
appId:
process.env.REACT_APP_AGORA_APPID || "default id",
channel: process.env.REACT_APP_AGORA_CHANNEL || "test",
token:
process.env.REACT_APP_AGORA_TOKEN ||
"default token",
uid: process.env.REACT_APP_AGORA_UID || "default uid",
};

const StreamHandler = (props) => {
// 组件参数: 用户信息, 当前频道所有消息, 当前频道id, 是否开启本地语音
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

const [rtcClient, setRtcClient] = useState(null);
// 声网client连接完成
const [connectStatus, setConnectStatus] = useState(false);

// RTC相关逻辑
useEffect(() => {
AgoraRTC.setLogLevel(3);
const client = AgoraRTC.createClient({ mode: "rtc", codec: "vp8" });
// TODO: use right channel
client
.join(options.appId, options.channel, options.token, userInfo?.username)
.then(() => {
setConnectStatus(true);
console.log("[Stream] join channel success");
})
.catch((e) => {
console.log(e);
});

setRtcClient(client);
return () => {
// 销毁时, 自动退出RTC频道
client.leave();
setRtcClient(null);
};
}, []);

return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
</>
);
}


// 我们需要全局状态中的userinfo, 映射一下到当前组件的props中
const mapStateToProps = ({ app }) => {
return {
userInfo: app.userInfo,
};
};
export default memo(connect(mapStateToProps)(StreamHandler));

然后回到Channel中, 在之前的renderStreamChannel函数中添加上StreamHandler组件

// view/Channel/index.js
const [enableVoice, setEnableVoice] = useState(false);
const toggleVoice = () => {
setEnableVoice((enable) => {
return !enable;
});
}

// 保留了输入窗口, 可以在它的菜单栏中添加游戏频道独有的一些逻辑,
// 这里我加入了开关本地语音的逻辑, 拓展Input的细节可以参照完整版代码
const renderStreamChannel = () => {
return (
<>
<div className={s.messageRowWrap}>
<StreamHandler messageInfo={messageInfo} channelId={channelId} enableLocalVoice={enableVoice} />
</div>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} extraMenuItems={renderStreamMenu()} />
</div>
</>
);
}

const renderStreamMenu = () => {
return [
{
key: "voice",
label: (
<div
className="circleDropItem"
onClick={toggleVoice}
>
<Icon
name="person_wave_slash"
size="24px"
iconClass="circleDropMenuIcon"
/>
<span className="circleDropMenuOp">
{enableVoice ? "关闭语音" : "开启语音"}
</span>
</div>
),
}
];
}

此时我们创建一个video-开题的游戏频道, 应该可以看到命令行中输出了RTC连接成功信息. [Stream] join channel success

音视频推流

接下来我们继续做实质的RTC推流逻辑, 及用户上下播的入口. 但在那之前, 先简单过一下声网RTC中的一些概念.

参考以下步骤实现音视频通话的逻辑:

  1. 调用 createClient 方法创建 AgoraRTCClient 对象。
  2. 调用 join 方法加入一个 RTC 频道,你需要在该方法中传入 App ID 、用户 ID、Token、频道名称。
  3. 先调用 createMicrophoneAudioTrack 通过麦克风采集的音频创建本地音频轨道对象,调用 createCameraVideoTrack 通过摄像头采集的视频创建本地视频轨道对象;然后调用 publish 方法,将这些本地音视频轨道对象当作参数即可将音视频发布到频道中。
  4. 当一个远端用户加入频道并发布音视频轨道时:
  5. 监听 client.on(“user-published”) 事件。当 SDK 触发该事件时,在这个事件回调函数的参数中你可以获取远端用户 AgoraRTCRemoteUser 对象 。
  6. 调用 subscribe 方法订阅远端用户 AgoraRTCRemoteUser 对象,获取远端用户的远端音频轨道 RemoteAudioTrack 和远端视频轨道 RemoteVideoTrack 对象。

在这里插入图片描述

(以上内容来自声网官方文档)

在上面的接入中, 我们已经完成了创建对象并加入频道两步.
在RTC中, 可以传输音频和视频信号, 由于单个RTC客户端要传输不同种类的数据, 每个单独的音视频源被分成不同的track(由于它们都是实时不断产生的, 我们称作流), 随后通过publish方法, 将我们本地的信号源交付给RTC客户端传输.
随后通过user-published事件的回调来在其他用户发布信号源时进行处理, 首先需要subscribe该用户来获取后续数据, 随后根据不同类型的信号流做处理.
离开时需要关闭本地当前的信号源, 并退出RTC客户端.
最后通过user-unpublished事件监听其他用户退出, 移除它们对应的信号流.

逻辑理清楚后代码就很容易看懂了

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
...
// 本地视频元素
const localVideoEle = useRef(null);
// 远程视频元素
const canvasEle = useRef(null);
const [rtcClient, setRtcClient] = useState(null);
const [connectStatus, setConnectStatus] = useState(false);
// 当前直播的用户
const [remoteUser, setRemoteUser] = useState(null);
// 远程音视频track
const [remoteVoices, setRemoteVoices] = useState([]);
const [remoteVideo, setRemoteVideo] = useState(null);

// RTC相关逻辑
useEffect(() => {
...
// client.join 后

// 监听新用户加入
client.on("user-published", async (user, mediaType) => {
// auto subscribe when users coming
await client.subscribe(user, mediaType);
console.log("[Stream] subscribe success on user ", user);
if (mediaType === "video") {
// 获取直播流
if (remoteUser && remoteUser.uid !== user.uid) {
// 只能有一个用户推视频流
console.error(
"already in a call, can not subscribe another user ",
user
);
return;
}
// 播放并记录下视频流
const remoteVideoTrack = user.videoTrack;
remoteVideoTrack.play(localVideoEle.current);
setRemoteVideo(remoteVideoTrack);
// can only have one remote video user
setRemoteUser(user);
}
if (mediaType === "audio") {
// 获取音频流
const remoteAudioTrack = user.audioTrack;
// 去重
if (remoteVoices.findIndex((item) => item.uid === user.uid) == -1) {
remoteAudioTrack.play();
// 添加到数组中
setRemoteVoices([
...remoteVoices,
{ audio: remoteAudioTrack, uid: user.uid },
]);
}
}
});

client.on("user-unpublished", (user) => {
// 用户离开, 去除流信息
console.log("[Stream] user-unpublished", user);
removeUserStream(user);
});
setRtcClient(client);
return () => {
client.leave();
setRtcClient(null);
};
}, []);

const removeUserStream = (user) => {
if (remoteUser && remoteUser.uid === user.uid) {
setRemoteUser(null);
setRemoteVideo(null);
}
setRemoteVoices(remoteVoices.filter((voice) => voice.uid !== user.uid));
};
}

接着我们根据之前提到的自定义消息判断当前在播状态, 以最后一条自定义消息为准.

// views/Channel/components/StreamHandler/index.js
const StreamHandler = (props) => {
const { userInfo, messageInfo, channelId, enableLocalVoice = false } = props;

// 第一条 stream 消息, 用于判断直播状态
const firstStreamMessage = useMemo(() => {
return messageInfo?.list?.find(
(item) => item.type === "custom" && item?.ext?.type === "stream"
);
}, [messageInfo]);

// 是否有直播
const hasRemoteStream =
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user !== userInfo?.username;
// 本地直播状态
const [localStreaming, setLocalStreaming] = useState(
firstStreamMessage?.ext?.status === CMD_START_STREAM &&
firstStreamMessage?.ext?.user === userInfo?.username
);

// 本地直播流状态
const toggleLocalGameStream = () => {
if (hasRemoteStream) {
return;
}
setLocalStreaming(!localStreaming);
};
// 根据直播状态选择渲染
return (
<>
{!connectStatus && <Spin tip="Loading" size="large" />}
{hasRemoteStream ? (
<RemoteStreamHandler
remoteUser={firstStreamMessage?.ext?.user}
localVideoRef={localVideoEle}
channelId={channelId}
userInfo={userInfo}
rtcClient={rtcClient}
/>
) : (
<LocalStreamHandler
localStreaming={localStreaming}
canvasRef={canvasEle}
toggleLocalGameStream={toggleLocalGameStream}
rtcClient={rtcClient}
userInfo={userInfo}
channelId={channelId}
/>
)}
</>
);
}

我们根据hasRemoteStream分成两种逻辑RemoteStreamHandlerLocalStreamHandler(可以先用div+文字的空实现占位), 首先我们来看本地游戏的逻辑

// view/Channel/components/StreamHandler/local_stream.js
const LocalStreamHandler = (props) => {

const {
toggleLocalGameStream,
canvasRef,
localStreaming,
rtcClient,
userInfo,
channelId,
} = props;

const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

const renderLocalStream = () => {
return (
<div style={{ height: "100%" }} ref={localPlayerContainerRef}>
</div>
)
}

// 控制上下播
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleLocalGameStream}
icon={
localStreaming ? <VideoCameraFilled /> : <VideoCameraOutlined />
}
tooltip={<div>{localStreaming ? "停止直播" : "开始直播"}</div>}
/>
</FloatButton.Group>
);
};

// 渲染: 悬浮窗和本地流
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderLocalStream()}
</div>
</>
);
}

现在我们进入直播房间已经可以看到本地摄像头的内容了, 但我们还没有将视频流投放到RTC中, 且上播逻辑也没有处理

// view/Channel/components/StreamHandler/local_stream.js
useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient || !localVideoStream) {
return;
}
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
// 将流publish到rtc中
rtcClient.publish(localVideoStream).then(() => {
// 频道中发布一条消息, 表示开始直播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
// 用户退出的清理工作,
// unpublish流(远程), 停止播放流(本地), 发送直播关闭消息(频道)
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId, localVideoStream]);

为了测试直播效果, 我们需要登录第二个账号(使用浏览器的匿名/开其他的浏览器, 此时cookie没有共享, 可以多账号登录), 进入相同频道, 开启直播, 此时第一个账号应该会自动刷新状态(如果没有则手动切换一下频道), 进入到RemoteStreamHandler, 说明我们直播的逻辑已经完成.

本地语音的逻辑也是类似的, 这里就不再重复.

接下来是远程流的渲染逻辑, 它的逻辑相对简单, 观看者可以选择开始/停止观看直播流

// view/Channel/components/StreamHandler/remote_stream.js
const RemoteStreamHandler = (props) => {

const {
remoteUser,
localVideoRef,
toggleRemoteVideo,
channelId,
userInfo,
rtcClient,
} = props;

// 这里加一个强制t人的开关, 由于debug
const enableForceStop = true;
const forceStopStream = () => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
};
const renderRemoteStream = () => {
return (
<div style={{ height: "100%" }}>
<div
id="remote-player"
style={{
width: "100%",
height: "90%",
border: "1px solid #fff",
}}
ref={localVideoRef}
/>
<div
style={{
display: "flex",
justifyContent: "center",
marginTop: "10px",
}}
>
<span style={{ color: "#0ECD0A" }}>{remoteUser}</span>
&nbsp; is playing{" "}
</div>
</div>
);
};
const renderFloatButtons = () => {
return (
<FloatButton.Group
icon={<DesktopOutlined />}
trigger="click"
style={{ left: "380px" }}
>
<FloatButton
onClick={toggleRemoteVideo}
icon={<VideoCameraAddOutlined />}
tooltip={<div>观看/停止观看直播</div>}
/>
{enableForceStop && (
<FloatButton
onClick={forceStopStream}
icon={<VideoCameraAddOutlined />}
tooltip={<div>强制停止直播</div>}
/>
)}
</FloatButton.Group>
);
};
return (
<>
<div style={{ height: "100%" }}>
{renderFloatButtons()}
{renderRemoteStream()}
</div>
</>
);
}

开关远程流的代码在StreamHander中, 作为参数传给RemoteStream

// views/Channel/components/StreamHandler/index.js
const toggleRemoteVideo = () => {
if (!hasRemoteStream) {
return;
}
console.log("[Stream] set remote video to ", !enableRemoteVideo);
// 当前是关闭状态,需要打开
// 开关远程音频的逻辑也与此类型.
if (enableRemoteVideo) {
remoteVideo?.stop();
} else {
remoteVideo?.play(localVideoEle.current);
}
setEnableRemoteVideo(!enableRemoteVideo);
};

ok, 现在我们已经实现了基于声网RTC, 在环信超级社区集成视频直播的功能.

直播替换为游戏流

接下来我们来将直播流升级一下, 替换成模拟器包, 为了方便测试, 我们直接使用打包好的版本(https://github.com/a71698422/web-0.1.1), pkg包解压后直接放置到项目根目录,

RustNESEmulator 是一个基于Rust语言的NES模拟器, 我们在web平台可以使用它编译好的wasm版本

并将mario.nes文件放到src/assets目录下, 这是初代马里奥游戏的ROM文件(你也可以使用你喜欢的nes游戏, 如果遇到问题, 欢迎到RustNESEmulator中提issue)

加入前端的模拟器适配代码

// views/Channel/components/StreamHandler
// from tetanes.

import * as wasm from "@/pkg";
class State {
constructor() {
this.sample_rate = 44100;
this.buffer_size = 1024;
this.nes = null;
this.animation_id = null;
this.empty_buffers = [];
this.audio_ctx = null;
this.gain_node = null;
this.next_start_time = 0;
this.last_tick = 0;
this.mute = false;
this.setup_audio();
console.log("[NES]: create state");
}

load_rom(rom) {
this.nes = wasm.WebNes.new(rom, "canvas", this.sample_rate);
this.run();
}

toggleMute() {
this.mute = !this.mute;
}

setup_audio() {
const AudioContext = window.AudioContext || window.webkitAudioContext;
if (!AudioContext) {
console.error("Browser does not support audio");
return;
}
this.audio_ctx = new AudioContext();
this.gain_node = this.audio_ctx.createGain();
this.gain_node.gain.setValueAtTime(1, 0);
}

run() {
const now = performance.now();
this.animation_id = requestAnimationFrame(this.run.bind(this));
if (now - this.last_tick > 16) {
this.nes.do_frame();
this.queue_audio();
this.last_tick = now;
}
}

get_audio_buffer() {
if (!this.audio_ctx) {
throw new Error("AudioContext not created");
}

if (this.empty_buffers.length) {
return this.empty_buffers.pop();
} else {
return this.audio_ctx.createBuffer(1, this.buffer_size, this.sample_rate);
}
}

queue_audio() {
if (!this.audio_ctx || !this.gain_node) {
throw new Error("Audio not set up correctly");
}

this.gain_node.gain.setValueAtTime(1, this.audio_ctx.currentTime);

const audioBuffer = this.get_audio_buffer();
this.nes.audio_callback(this.buffer_size, audioBuffer.getChannelData(0));
if (this.mute) {
return;
}
const source = this.audio_ctx.createBufferSource();
source.buffer = audioBuffer;
source.connect(this.gain_node).connect(this.audio_ctx.destination);
source.onended = () => {
this.empty_buffers.push(audioBuffer);
};
const latency = 0.032;
const audio_ctxTime = this.audio_ctx.currentTime + latency;
const start = Math.max(this.next_start_time, audio_ctxTime);
source.start(start);
this.next_start_time = start + this.buffer_size / this.sample_rate;
}
// ...
}

export default State;

改造local_stream

// view/Channel/components/StreamHandler/local_stream.js

import mario_url from "@/assets/mario.nes";
import * as wasm_emulator from "@/pkg";
import State from "./state";

const LocalStreamHandler = (props) => {
// 模拟器 state
const stateRef = useRef(new State());

// 注意要将原来的代码注释掉
/*
const [localVideoStream, setLocalVideoStream] = useState(false);
const localPlayerContainerRef = useRef(null);

// 开启本地视频流
useEffect(() => {
if (!localPlayerContainerRef.current) return;
const f = async () => {
// 暂时使用视频代替游戏流
let lgs = await AgoraRTC.createCameraVideoTrack();
lgs.play(localPlayerContainerRef.current);
setLocalGameStream(lgs);
}
f();
}, [localPlayerContainerRef])

// 推流的函数也暂时注释
useEffet...
*/


useEffect(() => {
// 本地游戏
if (!canvasRef) {
return;
}
// 开启键盘监听等全局事件
wasm_emulator.wasm_main();
fetch(mario_url, {
headers: { "Content-Type": "application/octet-stream" },
})
.then((response) => response.arrayBuffer())
.then((data) => {
let mario = new Uint8Array(data);
// 加载 rom数据
stateRef.current.load_rom(mario);
});
}, [canvasRef]);

// 更新本地流渲染
const renderLocalStream = () => {
return (
<div style={{ height: "100%" }}>
<canvas
id="canvas"
style={{ width: 600, height: 500 }}
width="600"
height="500"
ref={canvasRef}
/>
</div>
);
};
}

这一步完成后, 我们就可以在本地试玩马里奥游戏了, 键盘绑定为

A      = J
B = K
Select = RShift
Start = Return
Up = W
Down = S
Left = A
Right = D

将推本地视频流改为游戏流

  useEffect(() => {
// 发布直播推流
if (!localStreaming || !rtcClient) {
return;
}
// 只修改了流获取部分
// canvas的captureStream接口支持获取视频流
// 我们用这个视频流构造一个声网的自定义视频流
let stream = canvasRef.current.captureStream(30);
let localVideoStream = AgoraRTC.createCustomVideoTrack({
mediaStreamTrack: stream.getVideoTracks()[0],
});
console.log("height", canvasRef.current.height);
console.log("publishing local stream", localVideoStream);
rtcClient.publish(localVideoStream).then(() => {
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
).then(() => {
message.success({
content: "start streaming",
});
});
});
return () => {
if (localVideoStream) {
rtcClient.unpublish(localVideoStream);
localVideoStream.stop();
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);
message.info({
content: "stop streaming",
});
}
};
}, [rtcClient, localStreaming, canvasRef, userInfo, channelId]);

最后总结一下房间的流程图
在这里插入图片描述

至此该项目的完整流程就算结束啦,如果有哪些步骤细节不太明确, 可以参照完整版项目
环信超级社区项目
注册环信
模拟器直播项目github源码获取

收起阅读 »

实战:一天开发一款内置游戏直播的国产版Discord应用【附源码】(上)

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下: 开整!St...
继续阅读 »

游戏直播是Discord产品的核心功能之一,本教程教大家如何1天内开发一款内置游戏直播的国产版Discord应用,用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播,无任何实时音视频底层技术的Web开发者同样适用,效果如下:



开整!

Step1 初始化

本项目基于环信超级社区的实例项目, 所以我们先从Circle-Demo-Web这个仓库开启做初始化

  1. 克隆项目 git clone https://github.com/easemob/Circle-Demo-Web.git
  2. 安装依赖 npm install
  3. 设置appKey src/utils/WebIM.js 中设置appKey,AppKey为环信后台项目对应的key,注册环信,https://console.easemob.com/user/register,登录console后台获取Appkey
  4. 运行项目 npm run start

运行后, 登录完毕效果如下,
在这里插入图片描述

与discord设计逻辑相似, 左边功能区有

  • 个人信息页
  • 好友会话页
  • 当前加入的频道
  • 创建新频道
  • 加入服务器

超级社区的逻辑为

社区(Server)、频道(Channel) 和子区(Thread) 三层结构

社区为一个独立的结构, 不同社区直接相互隔离, 社区包含不同的频道, 代表了不同的话题, 用户在频道中聊天, 而针对一条单独信息产生的回复为子区.

我们本次的项目主要集中在频道部分, 需要加入一个服务器后, 创建一个测试社区, 保证你具有管理员权限.

Step2 协议设置

在这里插入图片描述

我们的目标是尽量利用现有api扩展功能, 有几个问题需要解决

  1. 如何区分普通频道和游戏频道?
  2. 如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?
  3. 多人聊天的状态?

如何区分普通频道和游戏频道

这里直接简单采用频道前缀做特殊区分, 创建频道前缀带video-的识别为游戏频道, 同时将渲染内容做替换

// views/Channel/index.js


const isVideoChannel = useMemo(() => {
return currentChannelInfo?.name?.startsWith("");
}, [currentChannelInfo]);

const renderTextChannel = () => {
// 原来的渲染逻辑
return (
<>
<MessageList
messageInfo
={messageInfo}
channelId
={channelId}
handleOperation
={handleOperation}
className
={s.messageWrap}
/>
<div className={s.iptWrap}>
<Input chatType={CHAT_TYPE.groupChat} fromId={channelId} />
</div>
</>
);
}

const renderStreamChannel = () => {
// 先填充一个占位符
return (
<>This is a Stream Channel<>
);
}

return (
...
<div className={s.contentWrap}>
{isVideoChannel ? renderStreamChannel() : renderTextChannel()}
</div>
...
);

如果需要区分图标, 可以搜索channelNameWrap, 分别在channelItemChannel/components/Header中添加一个css类, 通过这个类设置图标图片

如何区分当前频道是否有玩家直播, 如果有直播如何获取玩家信息, 第二玩家的状态?

我们可以复用在频道中发送消息的机制, 直播开始, 结束都可以当做一条特殊的消息发送, 只不过这条消息不承载用户的信息, 而是表达用户上下播的行为

当然这个机制存在一定实时性的问题, 不过大致是可行的.

首先我们来看一条普通的消息是如何发送的

  // components/input

//发消息
const sendMessage = useCallback(() => {
if (!text) return;
getTarget().then((target) => {
let msg = createMsg({
chatType,
type: "txt",
to: target,
msg: convertToMessage(ref.current.innerHTML),
isChatThread: props.isThread
});
setText("");
deliverMsg(msg).then(() => {
if (msg.isChatThread) {
setThreadMessage({
message: { ...msg, from: WebIM.conn.user },
fromId: target
});
} else {
insertChatMessage({
chatType,
fromId: target,
messageInfo: {
list: [{ ...msg, from: WebIM.conn.user }]
}
});
scrollBottom();
}
});
});
}, [text, props, getTarget, chatType, setThreadMessage, insertChatMessage]);

去除掉与输入框逻辑耦合的部分, 可以分为两步, createMsg创建消息, deliverMsg发送消息, 这两个功能都是环信SDK功能的封装, 经过查阅文档, 它支持发送自定义消息.
在utils中新建一个stream.js文件来封装直播的逻辑

// utils/stream.js
const sendStreamMessage = (content, channelId) => {
let msg = createMsg({
chatType: CHAT_TYPE.groupChat,
type: "custom",
to: channelId,
ext: {
type: "stream",
...content,
},
});
return deliverMsg(msg)
.then(() => {
console.log("发送成功");
})
.catch(console.error);
};

它接收content表示我们的额外信息, 用户名和上下播状态, channelId区分不同的channel, 对它的调用可以如下

// 定义在 utils/stream.js 中
const CMD_START_STREAM = "start";
const CMD_END_STREAM = "end";

// 上播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_START_STREAM,
},
channelId
);
// 下播
sendStreamMessage(
{
user: userInfo?.username,
status: CMD_END_STREAM,
},
channelId
);

第二玩家的状态可以类比第一个玩家用额外的自定义消息实现, 这里不做重复.

关于自定义消息, 原本它的作用是邀请用户加入频道, 你可以在components/CustomMsg中找到, 我们要额外识别一下直播消息(可以渲染在消息列表里, 也可以直接屏蔽掉).

// components/CustomMsg/index.js
const isStream = message?.ext?.type === "stream";


// 屏蔽
const renderStream = () => {
return (<>)
}
if (isStream) {
return renderStream();
} else {
...
}

多人聊天的状态?

我们引入声网RTC sdk, 每个进入直播房间的用户都对应维护一个声网客户端,
通过on事件感知远端视频/音频流.

根据文档 进行如下操作,

  1. 注册声网开发者, 并在后台创建一个测试项目
  2. 项目根目录创建.env文件, 存放api token等信息
# channel, uid 暂时设置为固定
REACT_APP_AGORA_APPID = your app id
REACT_APP_AGORA_CHANNEL = test
REACT_APP_AGORA_TOKEN = your token
REACT_APP_AGORA_UID = 123xxx
  1. 添加声网sdk依赖 npm install agora-rtc-sdk-ng

我们在下一章中编写接入逻辑

声网RTC接入, 直播与语音实现

收起阅读 »

【附源码】国内首届Discord场景创意编程开源项目

以下开源项目是由环信联合华为举办的《国内首届Discord场景创意编程赛》作品,附源码,一键即用。一、 模拟器游戏直播-新新人类新新人类模拟器游戏直播基于环信超级社区Demo构建,增加以“video-x”命名的新型Channel,用户可在本机操作/控制当前游戏...
继续阅读 »

以下开源项目是由环信联合华为举办的《国内首届Discord场景创意编程赛》作品,附源码,一键即用。


一、 模拟器游戏直播-新新人类

新新人类模拟器游戏直播基于环信超级社区Demo构建,增加以“video-x”命名的新型Channel,用户可在本机操作/控制当前游戏界面,并通过集成声网RTC SDK, 在聊天频道中实现连麦聊天, 一对多直播。其中直播流来自于NES模拟器画面, 用户可以观看房主游玩经典NES的游戏画面. 并进一步与房主联机, 实现2p游戏。


模拟器游戏直播-项目预览

该项目不仅集成了环信超级社区SDK,声网的RTC功能,也同时集成了第三方小游戏。而这正是超级社区,也就是Discord产品的精髓之一。用户不仅可以通过IM聊天,也可以进行语聊,看游戏直播,甚至自己进行游戏直播。这些都是当下Discord这款产品中使用率最高的功能。这个作品不仅让人眼前一亮,也展示出作者对Discord和超级社区场景深入的理解,令人印象深刻。

源码:https://github.com/easemob/EasemobCircle-2022-Innovation-Challenge/tree/master/Innovation-Challenge/%E6%96%B0%E6%96%B0%E4%BA%BA%E7%B1%BB-%E6%A8%A1%E6%8B%9F%E5%99%A8%E7%9B%B4%E6%92%AD


二、 代码搬运工-CT超级社区

CT超级社区基于环信超级社区Demo,在实时聊天场景基本功能中,丰富了聊天内容英译汉翻译功能。同时增加了频道插件功能,通过将封装部分API成SDK,部分功能可通过插件的形式去实现,通过丰富的插件功能提高用户互动性,提升社区体验。目前实现的插件有:投票、社区签到、打卡分享、代码分享、频道内置机器人、外置拓展机器人。

CT超级社区-项目预览

该项目集成了多个超级社区场景下的高使用频率功能,投票以及打卡签到、机器人等插件有助于提升社区活跃度,鼓励社区内用户发起讨论,独到之处也为开发者们提供了分享代码的功能插件,为社区提供了更丰富的互动元素,高度契合了超级社区场景化需求。

源码:https://github.com/easemob/EasemobCircle-2022-Innovation-Challenge/tree/master/Innovation-Challenge/%E4%BB%A3%E7%A0%81%E6%90%AC%E8%BF%90%E5%B7%A5-CT%E7%A4%BE%E5%8C%BA


三、小雪花-有趣点儿圈子

“有趣点儿圈子”基于环信超级社区构建,在于支持万人场景下的沟通交流娱乐。多种分类频道(通知频道、直播频道),满足于用户畅游。用户等级VIP信息,专属聊天图标。内置扔骰子游戏,石头剪刀布游戏,红包功能,随机打卡功能。还有可以陪你的机器人功能,萌萌的它,可以每日单词、笑话、天气提醒...,更有功能强大的ChatGPT AI对话。

有趣点儿圈子-项目预览


该项目集成了多种群内小游戏以及红包功能,同时支持了不同群成员的等级属性。作为一个社交类产品,这些功能都极大提升了一个社区的活力和丰富程度。值得一提的是虽然ChatGPT功能并没有在此项目中完全跑通,但这种创新精神和将新兴功能接入超级社区的想象力仍然值得鼓励。期待后期继续完善,为广大开发者分享更加卓越的场景应用。

源码:https://github.com/easemob/EasemobCircle-2022-Innovation-Challenge/tree/master/Innovation-Challenge/%E5%B0%8F%E9%9B%AA%E8%8A%B1-%E6%9C%89%E8%B6%A3%E7%82%B9%E5%84%BF%E5%9C%88%E5%AD%90

以上开源作品中使用到的SDK:

 ●注册环信:https://console.easemob.com/user/register

●超级社区介绍:https://www.easemob.com/product/im/circle

●超级社区SDK 集成文档::https://docs-im.easemob.com/ccim/circle/overview

●超级社区Demo体验:https://www.easemob.com/download/demo#discord

●技术支持社区:https://www.imgeek.org

收起阅读 »

类Discord应用『环信超级社区1.0』项目介绍【附源码】

2021年马斯克让Clubhouse火爆出圈,2022年Discord以1.5亿月活150亿美元估值的数据让全球的开发者们看到了泛娱乐领域新的机会,环信作为泛娱乐行业的基础设施服务商,一直致力于给开发者提供更稳定的SDK,更丰富更易用的API,更垂直的场景解决...
继续阅读 »

2021年马斯克让Clubhouse火爆出圈,2022年Discord以1.5亿月活150亿美元估值的数据让全球的开发者们看到了泛娱乐领域新的机会,环信作为泛娱乐行业的基础设施服务商,一直致力于给开发者提供更稳定的SDK,更丰富更易用的API,更垂直的场景解决方案。近日环信重磅推出了“环信超级社区DEMO”,这是一款类Discord产品的开源项目,在此基础上二开,可以快速搭建国内版Discord产品,帮您节省60%的开发难度!


项目介绍

环信超级社区是一款基于环信IM+声网RTC打造的类Discord实时社区应用,用户可创建/管理自己的兴趣社区,设置/管理频道(群组),支持陌生人/好友单聊、社区成员无上限,可创建的频道数无上限,用户加入的频道数无上限,真正实现万人实时群聊,语音聊天等。


功能架构


核心优势

1、IM提供高并发的通讯管道,支持亿级用户并发

▲万人群组互动

▲群组数量无上限

▲自定义加群权限设置

▲支持群资料和属性

▲提供群组/聊天室完善的群聊管理功能

▲提供管理员列表、成员列表、禁言列表、黑名单等服务

▲聊天室功能与直播功能进行对接实现直播聊天室

▲可以根据客户需要进行灵活配置,包括关系、数量、能力

2、百万人大群组承载

环信群组分片技术:将1个群中百万成员分片在100个万人群里


3、消息爆炸问题

解决方案:通过notice减少消息的Qps,进群后再拉取下发消息


4、环信SD-GMN,构建低延迟网络,实现全球加速

▲五大数据中心覆盖全球200+个国家和地区;

▲集团自建上万台服务器,部署全球300多个补充加速节点,实现低延迟;

▲FPA加速与AWS加速智能切换,确保通信质量和高可用能力;

▲典型时延:北美,30-40毫秒;欧洲20-30毫秒;东南亚,日韩30-40毫秒;中东70毫秒;北非45毫秒;澳洲50毫秒;最远的南美和南非,90毫秒;

▲持续改进,不断优化…

5、内容过滤能力

环信内容审核系统,低成本,高效率,个性化,高准确


适合场景

兴趣社交、游戏社交、区块链、媒体、粉丝社区、品牌社区等等。


项目源码

https://github.com/easemob/easemob_supercommunity


APK下载

链接: https://pan.baidu.com/s/1HUL_CUYTvUr3mT29WRcoaQ 提取码: zq1x 


超级社区2.0

继超级社区1.0以后,环信推出了超级社区2.0(Circle),这是一款基于环信 IM 打造的类 Discord 实时社区应用场景方案,支持社区(Server)、频道(Channel) 和子区(Thread) 三层结构。一个 App 下可以有多个社区,同时支持陌生人/好友单聊。用户可创建和管理自己的社区,在社区中设置和管理频道将一个话题下的子话题进行分区,在频道中根据感兴趣的某条消息发起子区讨论,实现万人实时群聊,满足超大规模用户的顺畅沟通需求。旨在一站式帮助客户快速开发和构建稳定超大规模用户即时通讯的"类Discord超级社区",作为构建实时交互社区的第一选择,环信超级社区自发布以来很好地满足了类 Discord 实时社区业务场景的客户需求,并支持开发者结合业务需要灵活自定义产品形态,目前已经广泛服务于国内头部出海企业以及海外东南亚和印度企业。

环信超级社区2.0介绍:https://www.easemob.com/product/im/circle

环信超级社区2.0体验:https://www.easemob.com/download/demo#discord


收起阅读 »

开源项目|使用声网&环信 SDK 构建元宇宙应用 MetaTown 最佳实践

大家好!我们是美特兄弟三人组!前阵参加了【声网&环信 RTE2022 创新编程挑战赛】,整个大赛历时47天,除了对声网和环信的 SDK 有了很多的体验,还做了很多奇思妙想的结合。在此次大赛中我们团队基于声网&环信 SDK 构建了一个元宇宙应用 ...
继续阅读 »

大家好!我们是美特兄弟三人组!前阵参加了【声网&环信 RTE2022 创新编程挑战赛】,整个大赛历时47天,除了对声网和环信的 SDK 有了很多的体验,还做了很多奇思妙想的结合。在此次大赛中我们团队基于声网&环信 SDK 构建了一个元宇宙应用 MetaTown,获得了环信专项奖,开心之余把这个项目介绍给大家,抛砖引玉,感兴趣的兄弟可以加入进来一起在元宇宙中闯荡江湖~

一、关于MetaTown

3f49489dc452a849d29829903a504934.jpg

金钱是被铸造出来的自由——陀思妥耶夫斯基

在三次元的现实世界,你是否为了搞钱而终日奔波?忍受996甚至007的非人待遇?是否正经历着创业人的凛冬?疫情等因素带来的本轮经济下行落实在每个人身上都是真真切切的,对于经历了40年经济暴增的国人来讲更是史无前例的。

在荷包日瘪萎靡不振的日子里,精神慰藉尤为重要,元宇宙正是当下最时髦的,何不创造一个可以躺着赚钱的元宇宙小镇?不为别的,在有虚拟工作的前提下每天我的虚拟角色的金币都会涨,想一想岂不是有一点小欢愉?还能在这个小镇结交一些志同道合沉迷于搞钱的友友们!

c1b918850b4200f52dd18a77f5787a35.jpg

自然这些虚拟财富目前还是无法转化为真正的成就感的(变为现实财富),但现在市面上开始有人吹web3.0了!坐等币圈大佬入局,我们有信心打造一款能让一部分人先富起来的元宇宙小镇!

---------------------------------------

MetaTown 是基于声网 RTC 和环信 IM 打造的模拟城市生活的元宇宙社交类 App。

初来乍到的玩家首次进入 MetaTown 先选择不同的职业,在这座城市首先要考虑的是如何赚钱,或做一名程序员上下班打卡,或自己创业开个酒吧/书店或去银行投资理财。除此以外,还要注意身体健康,可能哪天会随机生病需要去医院,支付挂号费咨询不同科室的医生。注意,没有核酸证明有可能看不了病嗷~不打工没钱也看不了病嗷~

c0b33d573cd7c33d59ffa94ff25aa85c.jpg

这个小镇的所有公共场所,均可以随时发起与陌生人私聊,