上海声网的实时语音sdk聊天sdk可以实现跨平台多设备连接吗?

2022年12月24日,人人都是产品经理举办的【2022产品经理大会】完美落幕。声网loT行业产品负责人@侯云忆 为我们带来了精彩的分享,她分享的主题是《物联网平台产品的修行和破局》。
今天分享的主题是物联网平台产品的修行和破局,希望我的分享可以给大家带来一定的启发和思维的碰撞。01 个人成长的坚持和初心1. 每一段自我革新和突破首先做一下自我介绍。我毕业于哈尔滨工业大学空间自主控制专业,在博士第三年发现自己不适合象牙塔后便退学进入了社会。从AI、智慧产业到现在的实时互动行业,一路不断突破自我。今天给大家带来的是我在做业务过程中的一些思考,以及物联网平台产品如何破局的案例分享。目前我在声网做 IoT 行业的产品负责人,加入一个在实时互动领域和实时音视频领域有着绝对领导者和开创者地位的公司,我所面临的课题是,如何在一个已经是行业赛道第一的平台,找到新赛道的突破。02 B端产品经理的修行1. 撕掉B端产品经理的标签和困惑相信很多 ToB 产品经理的成长过程中,都会遇到质疑声,工具人、传声筒、救火队、背锅侠,还有质疑你不懂技术的等等。让我常常思考技术边界和客户的需求如何平衡?产品的定制化交付和标准化之间又怎么权衡?我自己的个人价值到底在哪里?今天也是希望能够帮助大家一层一层地撕掉这样的标签和困惑。在声网,流行一句话——产品是一个公司的指挥棒。但是指挥棒不是管理层的专利,每个阶段该如何提炼你的专业度,这是我今天想告诉大家的。2. 人人都可以用好产品指挥棒不论你是在产品执行落地的管理阶段,还是能够面向整条业务线负责的阶段,你都要在不同阶段用好产品的器、术、道,影响你的整个产品和产品组织,让你的产品能说话。而在最基础的器方面,面向产品管理时,要有全生命周期的产品力,用一些产品设计和规划的套路,做面向产品功能和性能的迭代。过程中,可能需要你有一定的批判思维、数据思维和项目管理的能力,用你的专业度去影响组织内部的小伙伴,包括经常和你协同互动的研发、设计师、交付的同学和产品同学。当你真正能不面向产品本身做产品,而更进一步地思考一个大产品的过程时,你已经向上进阶到术的层面了,能够真正摆脱产品本身,走向业务,面向整个业务的商业闭环去负责。我们常常说一句话——当你面向客户时,才是产品交付和产品发挥价值真正开始的阶段。对外怎么向客户及外部提供产品价值,形成商业闭环?对内怎么让你的产品架构面向内部的业务效率和整个业务,去覆盖市场的规模,产生价值,能打大仗。这时你的产品领导力已经逐步影响你的客户、上下游的外部同学。不断进阶的过程中,逐渐从物到人,你所影响的范围也会从组织到行业、赛道。最高阶的是面向战略,做点线面体的不同战略设计和规划。通过资源和布局,为产品找到业务的变革和增长,以及有个人的行业影响力。从组织影响力到行业影响力,好的产品能帮你说话,为你产生这样的影响力。3. 举个例子:对话机器人最近 Chat GPT 非常火,如果你在体验 Chat GPT 的过程中,发现机器人经常一本正经地胡说八道,你会思考——这个模型看起来很厉害,但他怎么发挥商业价值呢?恭喜你,已经开始做到AI 产品化最底层的思维方式了。回顾一下,当年我们做对话机器人时,我在里面发挥的价值和产品能够给产品效率本身带来的价值是什么?1)面向产品功能在一个技术驱动的产品里,快速进入角色的一个点就是写test case。不要小看这件事情,以为它是测试的事情,事实上test case 既可以成为你和研发之间最好的沟通工具,能够了解你的产品价值和商业价值,还能帮助你了解技术边界。同时,AI 产品最基础的几个要素,有算法数据、场景和领域知识。产品在写 test case 的过程中,场景的构建和专家的领域知识这两件事情给算法、准确率本身带来的提升是远高于算法的境界的。所以第二个就是去找数据,做工具,以及完成面向客户的交付。有相似经验的同学可能很困扰——数据获取的成本是非常高的,它的准确性、离真实场景有多远,是非常难获取的。如何让真实用户在体验过程帮助你获得数据呢?比如我们做智能客服时,可以通过语义对机器人的满意度来获得数据的标签和打分,这时更多是为产品的准确率和功能迭代负责。2)面向业务效率通过对产品架构的掌握,面向客户和企业内部的业务效率去制定标准和流程,包括如何突破开放域的聊天对话这些不可控的点。我把机器人抽解到了十几个不同的算法模块,那么,解决不同的知识型问题和任务型问题时,该用到哪几个模块的最佳的组合呢?如何构建一个好的对话策略,并且快速构建场景所需要的术,做一些平台类的拖拉拽的工具,帮助不懂技术的业务人员也能快速构建一个对话机器人呢?做完这些,提升的是整个运营效率,并且让这件事情从流程变成行业标准。当你通过你的方法和工具沉淀出一个多年后业绩还在用的运营标准时,在“术”的层面就非常成功了。3)面向战略怎样通过对话机器人的能力,找到更多赛道和产生价值的点?在一个平台化的业务战略里,我抽象出了面向客服、企业内部、个人助理的不同对话机器人的形态。一方面它有不一样的内部原子能力的组合,快速地构建不同赛道和不同领域的极限;另一方面,当我形成了这三个标准之后,我的交付模型,还有商业变现的模型,也在不同的业务里自然地被抽象出来。最终是一个面向战略和资源去达成“道”的层面。在实践的过程当中,面对不同的产品,你的器、术、道的能力抽象会不一样。一是你逐渐从物的视角关注到人的视角,还有影响力的范围;二是看你是面向产品功能和性能层面在工作,还是沉淀到影响你行业标准的SOP或者方法论,以及你能否开辟赛道,掌握和整合资源。这个环节也有一句话送给大家——产品经理的成长是一场修行,希望在每个阶段大家都能掌握专业度,不论在企业内部,还是在行业当中,都可以挥好指挥棒。03 物联网平台产品的破局从 2020年开始,手机进入到负增长时代,全球物联网的设备超越了移动物联网的设备。作为声网IoT 行业的产品负责人,这时我们所面对的使命感,就是一个已经在移动互联网领域是 top 1 的技术平台,如何在物联网时代打开局面?1. 使命感:成为万物互联的实时通讯基础设施在声网,我们有一个愿景——让实时互动像水和空气一样,让每个开发者可以轻松地用到实时音视频,并且能够用好。我们发现在物联网赛道里,只要有屏幕或者 camera 的设备,都可以是我们的市场。而在信息密度增加和像疫情这样的黑天鹅时间之后,大家对于远程协作、安全、互动的需求,变得更加紧迫。可能不再是在移动的手机上,而是在像穿戴领域小朋友的手表,或者 AR/VR 的设备,都可以进行实时互动和远程的低延迟的通信。在家居场景中,智慧屏、智能音箱,都可以成为即时的影音入口。出行领域,智能车、机器人,户外的无人机设备,都可以成为远程通讯,及时获取流媒体的一个平台和设备。我们能够利用在移动互联网时代积累的技术给用户带来的在远程控制、远程巡检、协作上的良好体验,然而,想要实现从消费领域到产业互联网领域全面覆盖非常难。2. 然而,音视频物联网的市场挑战重重一是物联网市场大而老。市场盘子特别大,设备规模达到千亿级别,平均每个人可能会有7- 8个物联网设备。然而,大多数设备的性能,还有它对于 RTC 的认知度、需求度,当前还达不到痛点甚至是痒点的阶段。所以整个市场对 RTC、音视频的认知度是非常低的,设备性能也没有达到这个阶段。二是场景分散。不论是手表、AR、VR、车,都是千亿级、万亿级起的市场,每一个场景所在的玩家、产业链、技术的使用者和影响者都是不同的,所以场景非常分散,标准化困难。第三是产业链冗长。一个设备,从生产设计一直走到你面前,需要有上游的组件,像音视频的芯片、模组,还有我们所需要的传感器;在中游,可能会遇到硬件方的设备,品牌商也会需要有软件的平台;而在下游,既有消费级的应用平台,也会有面向产业级的行业解决方案商。在整版图当中,可以看到灰色箭头是一个硬件产品的生产过程,而白色箭头是技术服务,或者说互联网玩家在物联网的产业里找到机会点、融合点,产生价值的一个流。蓝色部分是直接使用者,即开发者和客户;而橙色部分,是产业里非常关键的影响方。而深橙色的一部分,既可能是你的客户,也可能是你的伙伴。产业链非常复杂,怎么快速让这些设备用到你,最终为客户产生价值是我们需要思考的问题。3. 去不同角色的困境中挖掘机会生长链条长,涉及角色多,怎样快速找到切入的点,是一个挑战。我们可以在刚才的角色中,挖掘一个机会,这就是你的技术和能力带来的价值。1)用户体验抓住产生决策的关键角色需求。终端用户关注的是我们能有一个实时及无感知的体验,体现在物联网设备里,会包括时延和出图这两个非常重要的因素。以及如何在一些设备上做到高质量的音质、画质和质量保障。还有很重要的是全球化,能够在全球任意地点的任意设备上感受到我们的产品。2)开发挑战刚才提到,物联网的玩家规模很大,但是在技术感知能力上比较老。因为很多物联网玩家,让他用端到端的音视频能力,需要嵌入式、客户端、云端这样不同的技术栈,很多传统硬件的开发者,甚至都找不到这么多技术栈的人,这对它的开发挑战是非常大的。物联网设备规模起来后,遇到的最大的挑战就是性能、价格和资源消耗之间的平衡。如何更低成本地用到高性能以及高信息密度的音视频服务,拥有快速接入能力,有好的连通率的保障和性能的保障,是大家遇到的一个很大的痛点。第三,如何体现音视频的差异化?同样的设备、性能及现象,如何让体验做到最好?这是留给行业的挑战,也是我们能够挖掘的机会点。3)产业壁垒当下我们每个人都有7-8个硬件设备,不同品牌生态之间不互通,这既是生态平台面向用户时的一个痛点,也是很多芯片原厂和硬件开发商的痛点。我们怎么样通过音视频、技术服务的点去破局呢?4. 声网IoT的点线面体声网做到 ,那就是在物联网领域破局的点线面体。点:以我们最擅长的音视频和实时通讯的能力为点,面向开发者的客户提供核心的体验,多场景建立标杆,深挖市场。
线:光有一个好的技术服务用不起来也不行,如何降低它的易用性和开发门槛?声网通过提供产品组合的方式,把一个个点串成线,让它在一个场景化的组合里快速地拥有完整性和易用性。
面:就是平台化,我们已经拥有以音视频为入口,提供除了音视频之外的从基础服务到全套增值服务的能力。这时不再只是面向设备提供连接能力,有很多向开发者提供转化的增值能力,也就是从单点技术走到提供平台服务的一个技术平台。
体:最后就是生态化,我们从直接面向客户本身,到面向客户的上下游的生态,到最后帮助生态成功。这已经跳出了产品本身,在看一个更广的大产品。
从点到线,也是我们在市场教育的过程中,能够给物联网的平台、设备带来什么样价值。以互动陪伴的设备举例。一个智能宠物陪伴需要远程控制、云台控制能力,也需要对于宠物的监测能力,以及录制、抓拍能力。在这个过程中,我们提供了一个完整的端到端能力,既满足了开发者对于各种技术栈的需求,也通过最佳实践,面向行业提供了高连通率、秒开出图/高实时性和可拓展性的能力。让开发者在我们的平台上就可以找到做智能化双赢所有的能力。再从线到面,是以开发者为破局点。以刚才的场景举例,过去开发者要完成一个智能硬件平台的开发,可能需要一个自研的 LD 平台,这时找到三方的音视频服务,比如声网或者其他友商,就是一类开发方式。第二类开发方式,很多云服务公司已经提供了包有一定 pup 连接能力的平台。这两种方式对于当前的用户或者开发者来讲,都有一定的挑战。前者会遇到开发门槛高的问题。通常来说只有在自研 IoT 平台的一些头部品牌商和硬件平台上才能进行,也会遇到硬件方案选型的往返、反复的过程。我们遇到过非常多的开发者,都是在硬件选型几乎完成的过程中,发现音视频开发的种种挑战,又回过头来改方案,这对于他来讲是一个非常大的挑战。如果是后者,就回到了一开始,我们提到这个行业对 RTC 没有认知度,还停留在之前点对点的传输方式,是没有体验保障的,所以它可以相对较快地完成功能的开发,但是功能的同质化很严重,音视频体验也不足以让用户随时地进行远程互动。我们发现市面上没有一个体验好用的音视频互联网平台,便主导做了一个 aPaaS 级的平台产品,叫做声网灵隼,它是一个为音视频硬件开发而生的物联网平台。基于过去提供的单点能力,我们把音视频的通讯和云信令进行了轻应用级的分装,提供能够解决开发者开发困难的端到端的平台型产品,包括覆盖到设备端、客户端和服务端的一些迁移用户的能力。除了音视频之外,还包含呼叫服务、云存服务、报警服务和设备管理。这样一个音视频的设备,基本上最快在 15 分钟内就可以跑通demo,开发的整个周期也会从过去的半年以上缩短到个把月,这也是我们在平台化的产品中,为客户和开发者带来的价值。另外一个是利用我们自己的 web RTC 开放协议来对接三方生态,像海外的 Alexa 、Google assistant 这样的智能音箱,这个思路是对标了现在全球物联网的 matter这样的跨生态的协议。所以未来只要是一个标准 web RTC,我们就可以很轻易地帮开发者跨平台和跨终端地互联互通。gle assistant 这样的智能音箱,这个思路是对标了现在全球物联网的 matter这样的跨生态的协议。所以未来只要是一个标准 web RTC,我们就可以很轻易地帮开发者跨平台和跨终端地互联互通。最后是如何帮助整个市场规模加速,帮助产业缩短产业链之间的鸿沟。我们在生态化上做了两件事情,一是用上游思维,找到在各个垂直领域赛道非常有优势的一些主流平台,结合刚才的场景化和平台化的战略,跟不同的平台去做场景化的深度适配。像无人车和机器人领域,我们选择英伟达;在手表领域选择了绝对第一的紫光展锐,音视频摄像头、门铃选择了像乐兴、博流这样的平台。一方面能够让我们的上游厂商像卖模组一样去卖我们的能力,加速整个市场覆盖;另一方面是解决了开发者的困扰。解决了上游之后,还有其他能力的伙伴,像AI、视觉类、音频类的能力。当我们落地到不同场景,比如智慧社区、智慧工业这些不一样的垂直领域,能够把我们的能力转化为他们行业经验的ISV。除了面向客户,我们也定义了面向生态伙伴的部分开源规划。用了部分开源之后,行业ISV 还可以用我们的 SDK 去做从 Turkey SDK 到 Turkey solution 的演进。这样我们的产品就成为一个共创的连接器,通过部分开源和一些轻松 ECO 的接口,帮助市场上下游伙伴和中游伙伴快速覆盖市场。5. 实时互动如何改变物联网通过不断向客户的上下游生态拓展,帮助生态成功,我们通过实时互动助力物联网由卖设备转向卖服务的变革,由设备能够面向一个垂直领域提供服务,并且任何一个设备都可以化身视频管家,或者家庭陪伴设备,帮助产品实现服务整体溢价。还有在 ToB 端带来的改变。比如帮助无人机厂在农林巡检、应急救灾、消防场景做到远程、实时、精准指挥和调度。让无人车、无人值守设备,成为了重要的人机协作和人工接管的通道,可以进行远程的沉浸式的控制。通过远程控制、场景智联的方式来改变人们的工作、生活方式。这些都是我们实实在在能看到的,现在的生产力带来的一些改变,和体现出的产品价值。我们相信,只要有摄像头和有屏幕的地方,就是互动界面,这也是一个跨终端的沉浸式的交互体验的案例。6. 最终,实时互动渗透在IoT的全场景总结一下,一是从技术层面,我们提供了从准实时到真实时的技术能力;二是过去我们更多地关注在流媒体本身,现在是面向不同设备之间的沉浸交互的体验。很多实时互动带来的变化,逐渐从消费物联网到产业物联网。这个能力从面向连接到面向服务,过去给 ToB 端带来的是生产关系的重构,现在是能够做生产力的改变。最后是面向行业的生态割裂的僵局,我们能够做到开放互联,价值本身已经不体现在某一个功能或具象的产品上,而是在面向于社会价值、大产品以及世界观的点上。不管是产品经理本身在进阶的过程,还是所做的产品,在我们发挥更多价值的时候,大家也可以审视一下是不是真的做到了。04 one more thing不论是职业还是我们所做的产品,最大的风险就是趋势,但是最大的机会也藏在趋势里。所以希望大家不仅是着眼于自己手头正在做的某一个产品,或者是正处于的行业,也可以站在现在更多地看一下未来。如果 IoT是人类感知的延伸,我们现在做的实时互动就是数字世界的中枢神经,整个世界就是你的屏幕。让我们一起拥抱变化吧下一步是什么?是 web 3. 0 吗?是元宇宙吗?可能1000个人心中有 1000 个对元宇宙的理解。通过这几段不一样的产品经历跟体验,我也拥有我的世界观。实际上,我所做的物联网就是一个最重要的感知能力,而我们的实时通讯能力就是元宇宙里最重要的中枢神经和管道。而我早期做的 AI 感知能力,就是结合三者之后就形成的一个新的世界观在元宇宙里从端到端的拆解,面向行业提供不一样的价值。如果 IoT是人类感知的延伸,我们现在做的实时互动就是数字世界的中枢神经,整个世界就是你的屏幕。当然 XR 可能会是下一代的计算平台,我个人也是以这样的心态来面对未来可能带来的改变和趋势。年度行业大会产品经理大会举办至今,已经成为了产品经理职业发展的风向标。每一年的大会,我们都聚焦行业发展的前沿动态、热点趋势,重点围绕产品经理人群的职业发展,为大家带来新方向、新思考。2023年度产品经理大会,在各自领域已经取得结果的嘉宾们将现身说法,从不同的视角告诉你如何应对当下的需求变化,探寻产品经理的进化方向!目前,北京站https://996.pm/7gX2B,深圳站https://996.pm/zaGbp,大会视频回放已上架,戳链接购买,即可回看!↓↓扫码加入大会咨询群↓↓了解最新嘉宾阵容、议程、购票优惠等本文为【2022产品经理大会】现场分享整理内容,由人人都是产品经理运营 @Estella 整理发布。未经许可,禁止转载,谢谢合作题图来自Unsplash,基于CC0协议。该文观点仅代表作者本人,人人都是产品经理平台仅提供信息存储空间服务。}
互动直播是实现很多热门场景的基础,例如直播带货、秀场直播,还有类似抖音的直播 PK等。本文是由声网社区的开发者“小猿”撰写的Flutter基础教程系列中的第二篇,他将带着大家用一个小时,利用声网 Flutter SDK 实现视频直播、发评论、送礼物等基础功能。上一篇介绍了如何实现多人视频互动。开发一个跨平台的的直播的功能需要多久?如果直播还需要支持各种互动效果呢?我给出的答案是不到一个小时,在 Flutter + 声网 SDK 的加持下,你可以在一个小时之内就完成一个互动直播的雏形。
声网作为最早支持 Flutter 平台的 SDK 厂商之一, 其 RTC SDK 实现主要来自于封装好的 C/C++ 等 native 代码,而这些代码会被打包为对应平台的动态链接库,最后通过 Dart 的 FFI(ffigen) 进行封装调用,减少了 Flutter 和原生平台交互时在 Channel 上的性能开销。
接下来让我们进入正题,既然选择了 Flutter + 声网的实现路线,那么在开始之前肯定有一些需要准备的前置条件,首先是为了满足声网 RTC SDK 的使用条件,开发环境必须为:Flutter 2.0 或更高版本
Dart 2.14.0 或更高版本
从目前 Flutter 和 Dart 版本来看,上面这个要求并不算高,然后就是你需要注册一个声网开发者账号 ,从而获取后续配置所需的 App ID 和 Token 等配置参数。
如果对于配置“门清”,可以忽略跳过这部分直接看下一章节。
创建项目首先可以在声网控制台的项目管理页面上点击创建项目,然后在弹出框选输入项目名称,之后选择「互动直播」场景和「安全模式(APP ID + Token)」 即可完成项目创建。
根据法规,创建项目需要实名认证,这个必不可少,另外使用场景不必太过纠结,项目创建之后也是可以根据需要自己修改。
获取 App ID在项目列表点击创建好的项目配置,进入项目详情页面之后,会看到基本信息栏目有个 App ID 的字段,点击如下图所示图标,即可获取项目的 App ID。
App ID 也算是敏感信息之一,所以尽量妥善保存,避免泄密。
获取 Token为提高项目的安全性,声网推荐了使用 Token 对加入频道的用户进行鉴权,在生产环境中,一般为保障安全,是需要用户通过自己的服务器去签发 Token,而如果是测试需要,可以在项目详情页面的「临时 token 生成器」获取临时 Token:
在频道名输入一个临时频道,比如 Test2 ,然后点击生成临时 token 按键,即可获取一个临时 Token,有效期为 24 小时。
这里得到的 Token 和频道名就可以直接用于后续的测试,如果是用在生产环境上,建议还是在服务端签发 Token ,签发 Token 除了 App ID 还会用到 App 证书,获取 App 证书同样可以在项目详情的应用配置上获取。
更多服务端签发 Token 可见 token server 文档 。
通过前面的配置,我们现在拥有了 App ID、 频道名和一个有效的临时 Token ,接下里就是在 Flutter 项目里引入声网的 RTC SDK :agora_rtc_engine 。项目配置首先在 Flutter 项目的 pubspec.yaml文件中添加以下依赖,其中 agora_rtc_engine 这里引入的是6.1.0版本 。
其实 permission_handler 并不是必须的,只是因为视频通话项目必不可少需要申请到麦克风和相机权限,所以这里推荐使用 permission_handler来完成权限的动态申请。
dependencies:
flutter:
sdk: flutter
agora_rtc_engine: ^6.1.0
permission_handler: ^10.2.0
这里需要注意的是, Android 平台不需要特意在主工程的 AndroidManifest.xml文件上添加uses-permission ,因为 SDK 的 AndroidManifest.xml 已经添加过所需的权限。iOS和macOS可以直接在Info.plist文件添加NSCameraUsageDescription和NSCameraUsageDescription的权限声明,或者在 Xcode 的 Info 栏目添加Privacy - Microphone Usage Description和Privacy - Camera Usage Description。
<key>NSCameraUsageDescription</key>
<string>*****</string>
<key>NSMicrophoneUsageDescription</key>
<string>*****</string>
使用声网 SDK获取权限在正式调用声网 SDK 的 API 之前,首先我们需要申请权限,如下代码所示,可以使用permission_handler的request提前获取所需的麦克风和摄像头权限。@override
void initState() {
super.initState();
_requestPermissionIfNeed();
}
Future<void> _requestPermissionIfNeed() async {
await [Permission.microphone, Permission.camera].request();
}
因为是测试项目,默认我们可以在应用首页就申请获得。
初始化引擎接下来开始配置 RTC 引擎,如下代码所示,通过 import 对应的 dart 文件之后,就可以通过 SDK 自带的 createAgoraRtcEngine 方法快速创建引擎,然后通过 initialize方法就可以初始化 RTC 引擎了,可以看到这里会用到前面创建项目时得到的 App ID 进行初始化。
注意这里需要在请求完权限之后再初始化引擎。
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
late final RtcEngine _engine;
Future<void> _initEngine() async {
_engine = createAgoraRtcEngine();
await _engine.initialize(const RtcEngineContext(
appId: appId,
));
···
}
接着我们需要通过 registerEventHandler注册一系列回调方法,在 RtcEngineEventHandler 里有很多回调通知,而一般情况下我们比如常用到的会是下面这几个:onError :判断错误类型和错误信息
onJoinChannelSuccess:加入频道成功
onUserJoined:有用户加入了频道
onUserOffline:有用户离开了频道
onLeaveChannel:离开频道
onStreamMessage: 用于接受远端用户发送的消息
Future<void> _initEngine() async {
···
_engine.registerEventHandler(RtcEngineEventHandler(
onError: (ErrorCodeType err, String msg) {},
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
setState(() {
isJoined = true;
});
},
onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
remoteUid.add(rUid);
setState(() {});
},
onUserOffline:
(RtcConnection connection, int rUid, UserOfflineReasonType reason) {
setState(() {
remoteUid.removeWhere((element) => element == rUid);
});
},
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
setState(() {
isJoined = false;
remoteUid.clear();
});
},
onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
Uint8List data, int length, int sentTs) {
}));
用户可以根据上面的回调来判断 UI 状态,比如当前用户时候处于频道内显示对方的头像和数据,提示用户进入直播间,接收观众发送的消息等。
接下来因为我们的需求是「互动直播」,所以就会有观众和主播的概念,所以如下代码所示:首先需要调用enableVideo 打开视频模块支持,可以看到视频画面
同时我们还可以对视频编码进行一些简单配置,比如通过 VideoEncoderConfiguration 配置分辨率是帧率
根据进入用户的不同,我们假设type为"Create"是主播, **"Join"**是观众
那么初始化时,主播需要通过通过startPreview开启预览
观众需要通过enableLocalAudio(false); 和enableLocalVideo(false);关闭本地的音视频效果
Future<void> _initEngine() async {
···
_engine.enableVideo();
await _engine.setVideoEncoderConfiguration(
const VideoEncoderConfiguration(
dimensions: VideoDimensions(width: 640, height: 360),
frameRate: 15,
),
);
/// 自己直播才需要预览
if (widget.type == "Create") {
await _engine.startPreview();
}
if (widget.type != "Create") {
_engine.enableLocalAudio(false);
_engine.enableLocalVideo(false);
}
关于 setVideoEncoderConfiguration 的更多参数配置支持如下所示:接下来需要初始化一个 VideoViewController,根据角色的不同:主播可以通过VideoViewController直接构建控制器,因为画面是通过主播本地发出的流
观众需要通过VideoViewController.remote构建,因为观众需要获取的是主播的信息流,区别在于多了connection 参数需要写入channelId,同时VideoCanvas需要写入主播的uid 才能获取到画面
late VideoViewController rtcController;
Future<void> _initEngine() async {
···
rtcController = widget.type == "Create"
? VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(uid: 0),
)
: VideoViewController.remote(
rtcEngine: _engine,
connection: const RtcConnection(channelId: cid),
canvas: VideoCanvas(uid: widget.remoteUid),
);
setState(() {
_isReadyPreview = true;
});
最后调用 joinChannel加入直播间就可以了,其中这些参数都是必须的:token 就是前面临时生成的Token
channelId 就是前面的渠道名
uid 就是当前用户的id ,这些id 都是我们自己定义的
channelProfile根据角色我们可以选择不同的类别,比如主播因为是发起者,可以选择channelProfileLiveBroadcasting ;而观众选channelProfileCommunication
clientRoleType选择clientRoleBroadcaster
Future<void> _initEngine() async {
···
await _joinChannel();
}
Future<void> _joinChannel() async {
await _engine.joinChannel(
token: token,
channelId: cid,
uid: widget.uid,
options: ChannelMediaOptions(
channelProfile: widget.type == "Create"
? ChannelProfileType.channelProfileLiveBroadcasting
: ChannelProfileType.channelProfileCommunication,
clientRoleType: ClientRoleType.clientRoleBroadcaster,
// clientRoleType: widget.type == "Create"
//
? ClientRoleType.clientRoleBroadcaster
//
: ClientRoleType.clientRoleAudience,
),
);
之前我以为观众可以选择 clientRoleAudience 角色,但是后续发现如果用户是通过 clientRoleAudience 加入可以直播间,onUserJoined 等回调不会被触发,这会影响到我们后续的开发,所以最后还是选择了 clientRoleBroadcaster。
渲染画面接下来就是渲染画面,如下代码所示,在 UI 上加入 AgoraVideoView控件,并把上面初始化成功的RtcEngine和VideoViewController配置到 AgoraVideoView,就可以完成画面预览。Stack(
children: [
AgoraVideoView(
controller: rtcController,
),
Align(
alignment: const Alignment(-.95, -.95),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.of(remoteUid.map(
(e) => Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.blueAccent),
alignment: Alignment.center,
child: Text(
e.toString(),
style: const TextStyle(
fontSize: 10, color: Colors.white),
),
),
)),
),
),
),
这里还在页面顶部增加了一个 SingleChildScrollView ,把直播间里的观众 id 绘制出来,展示当前有多少观众在线。
接着我们只需要在做一些简单的配置,就可以完成一个简单直播 Demo 了,如下图所示,在主页我们提供 Create 和 Join 两种角色进行选择,并且模拟用户的 uid 来进入直播间:主播只需要输入自己的 uid 即可开播
观众需要输入自己的 uid 的同时,也输入主播的 uid ,这样才能获取到主播的画面
接着我们只需要通过 Navigator.push 打开页面,就可以看到主播(左)成功开播后,观众(右)进入直播间的画面效果了,这时候如果你看下方截图,可能会发现观众和主播的画面是镜像相反的。如果想要主播和观众看到的画面是一致的话,可以在前面初始化代码的 VideoEncoderConfiguration 里配置 mirrorMode 为 videoMirrorModeEnabled,就可以让主播画面和观众一致。
await _engine.setVideoEncoderConfiguration(
const VideoEncoderConfiguration(
dimensions: VideoDimensions(width: 640, height: 360),
frameRate: 15,
bitrate: 0,
mirrorMode: VideoMirrorModeType.videoMirrorModeEnabled,
),
);
这里 mirrorMode 配置不需要区分角色,因为 mirrorMode 参数只会只影响远程用户看到的视频效果。
上面动图左下角还有一个观众进入直播间时的提示效果,这是根据 onUserJoined 回调实现,在收到用户进入直播间后,将 id 写入数组,并通过PageView进行轮循展示后移除。
前面我们已经完成了直播的简单 Demo 效果,接下来就是实现「互动」的思路了。前面我们初始化时注册了一个 onStreamMessage 的回调,可以用于主播和观众之间的消息互动,那么接下来主要通过两个「互动」效果来展示如果利用声网 SDK 实现互动的能力。首先是「消息互动」:我们需要通过 SDK 的createDataStream 方法得到一个streamId
然后把要发送的文本内容转为Uint8List
最后利用sendStreamMessage 就可以结合streamId 就可以将内容发送到直播间
streamId = await _engine.createDataStream(
const DataStreamConfig(syncWithAudio: false, ordered: false));
final data = Uint8List.fromList(
utf8.encode(messageController.text));
await _engine.sendStreamMessage(
streamId: streamId, data: data, length: data.length);
在 onStreamMessage 里我们可以通过utf8.decode(data) 得到用户发送的文本内容,结合收到的用户 id ,根据内容,我们就可以得到如下图所示的互动消息列表。onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
Uint8List data, int length, int sentTs) {
var message = utf8.decode(data);
doMessage(remoteUid, message);
}));
前面显示的 id ,后面对应的是用户发送的文本内容
那么我们再进阶一下,收到用户一些「特殊格式消息」之后,我们可以展示动画效果而不是文本内容,例如:
在收到 [ *** ] 格式的消息时弹出一个动画,类似粉丝送礼。
实现这个效果我们可以引入第三方 rive 动画库,这个库只要通过 RiveAnimation.network 就可以实现远程加载,这里我们直接引用一个社区开放的免费 riv 动画,并且在弹出后 3s 关闭动画。
showAnima() {
showDialog(
context: context,
builder: (context) {
return const Center(
child: SizedBox(
height: 300,
width: 300,
child: RiveAnimation.network(
'https://public.rive.app/community/runtime-files/4037-8438-first-animation.riv',
),
),
);
},
barrierColor: Colors.black12);
Future.delayed(const Duration(seconds: 3), () {
Navigator.of(context).pop();
});
}
最后,我们通过一个简单的正则判断,如果收到 [ *** ] 格式的消息就弹出动画,如果是其他就显示文本内容,最终效果如下图动图所示。
bool isSpecialMessage(message) {
RegExp reg = RegExp(r"[*]$");
return reg.hasMatch(message);
}
doMessage(int id, String message) {
if (isSpecialMessage(message) == true) {
showAnima();
} else {
normalMessage(id, message);
}
}
虽然代码并不十分严谨,但是他展示了如果使用声网 SDK 实现 「互动」的效果,可以看到使用声网 SDK 只需要简单配置就能完成「直播」和 「互动」两个需求场景。完整代码如下所示,这里面除了声网 SDK 还引入了另外两个第三方包:flutter_swiper_view 实现用户进入时的循环播放提示
rive用于上面我们展示的动画效果
import 'dart:async';
import 'dart:convert';
import 'dart:typed_data';
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:flutter_swiper_view/flutter_swiper_view.dart';
import 'package:rive/rive.dart';
const token = "xxxxxx";
const cid = "test";
const appId = "xxxxxx";
class LivePage extends StatefulWidget {
final int uid;
final int? remoteUid;
final String type;
const LivePage(
{required this.uid, required this.type, this.remoteUid, Key? key})
: super(key: key);
@override
State<StatefulWidget> createState() => _State();
}
class _State extends State<LivePage> {
late final RtcEngine _engine;
bool _isReadyPreview = false;
bool isJoined = false;
Set<int> remoteUid = {};
final List<String> _joinTip = [];
List<Map<int, String>> messageList = [];
final messageController = TextEditingController();
final messageListController = ScrollController();
late VideoViewController rtcController;
late int streamId;
final animaStream = StreamController<String>();
@override
void initState() {
super.initState();
animaStream.stream.listen((event) {
showAnima();
});
_initEngine();
}
@override
void dispose() {
super.dispose();
animaStream.close();
_dispose();
}
Future<void> _dispose() async {
await _engine.leaveChannel();
await _engine.release();
}
Future<void> _initEngine() async {
_engine = createAgoraRtcEngine();
await _engine.initialize(const RtcEngineContext(
appId: appId,
));
_engine.registerEventHandler(RtcEngineEventHandler(
onError: (ErrorCodeType err, String msg) {},
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
setState(() {
isJoined = true;
});
},
onUserJoined: (RtcConnection connection, int rUid, int elapsed) {
remoteUid.add(rUid);
var tip = (widget.type == "Create")
? "$rUid 来了"
: "${connection.localUid} 来了";
_joinTip.add(tip);
Future.delayed(const Duration(milliseconds: 1500), () {
_joinTip.remove(tip);
setState(() {});
});
setState(() {});
},
onUserOffline:
(RtcConnection connection, int rUid, UserOfflineReasonType reason) {
setState(() {
remoteUid.removeWhere((element) => element == rUid);
});
},
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
setState(() {
isJoined = false;
remoteUid.clear();
});
},
onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
Uint8List data, int length, int sentTs) {
var message = utf8.decode(data);
doMessage(remoteUid, message);
}));
_engine.enableVideo();
await _engine.setVideoEncoderConfiguration(
const VideoEncoderConfiguration(
dimensions: VideoDimensions(width: 640, height: 360),
frameRate: 15,
bitrate: 0,
mirrorMode: VideoMirrorModeType.videoMirrorModeEnabled,
),
);
/// 自己直播才需要预览
if (widget.type == "Create") {
await _engine.startPreview();
}
await _joinChannel();
if (widget.type != "Create") {
_engine.enableLocalAudio(false);
_engine.enableLocalVideo(false);
}
rtcController = widget.type == "Create"
? VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(uid: 0),
)
: VideoViewController.remote(
rtcEngine: _engine,
connection: const RtcConnection(channelId: cid),
canvas: VideoCanvas(uid: widget.remoteUid),
);
setState(() {
_isReadyPreview = true;
});
}
Future<void> _joinChannel() async {
await _engine.joinChannel(
token: token,
channelId: cid,
uid: widget.uid,
options: ChannelMediaOptions(
channelProfile: widget.type == "Create"
? ChannelProfileType.channelProfileLiveBroadcasting
: ChannelProfileType.channelProfileCommunication,
clientRoleType: ClientRoleType.clientRoleBroadcaster,
// clientRoleType: widget.type == "Create"
//
? ClientRoleType.clientRoleBroadcaster
//
: ClientRoleType.clientRoleAudience,
),
);
streamId = await _engine.createDataStream(
const DataStreamConfig(syncWithAudio: false, ordered: false));
}
bool isSpecialMessage(message) {
RegExp reg = RegExp(r"[*]$");
return reg.hasMatch(message);
}
doMessage(int id, String message) {
if (isSpecialMessage(message) == true) {
animaStream.add(message);
} else {
normalMessage(id, message);
}
}
normalMessage(int id, String message) {
messageList.add({id: message});
setState(() {});
Future.delayed(const Duration(seconds: 1), () {
messageListController
.jumpTo(messageListController.position.maxScrollExtent + 2);
});
}
showAnima() {
showDialog(
context: context,
builder: (context) {
return const Center(
child: SizedBox(
height: 300,
width: 300,
child: RiveAnimation.network(
'https://public.rive.app/community/runtime-files/4037-8438-first-animation.riv',
),
),
);
},
barrierColor: Colors.black12);
Future.delayed(const Duration(seconds: 3), () {
Navigator.of(context).pop();
});
}
@override
Widget build(BuildContext context) {
if (!_isReadyPreview) return Container();
return Scaffold(
appBar: AppBar(
title: const Text("LivePage"),
),
body: Column(
children: [
Expanded(
child: Stack(
children: [
AgoraVideoView(
controller: rtcController,
),
Align(
alignment: const Alignment(-.95, -.95),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: List.of(remoteUid.map(
(e) => Container(
width: 40,
height: 40,
decoration: const BoxDecoration(
shape: BoxShape.circle, color: Colors.blueAccent),
alignment: Alignment.center,
child: Text(
e.toString(),
style: const TextStyle(
fontSize: 10, color: Colors.white),
),
),
)),
),
),
),
Align(
alignment: Alignment.bottomLeft,
child: Container(
height: 200,
width: 150,
decoration: const BoxDecoration(
borderRadius:
BorderRadius.only(topRight: Radius.circular(8)),
color: Colors.black12,
),
padding: const EdgeInsets.only(left: 5, bottom: 5),
child: Column(
children: [
Expanded(
child: ListView.builder(
controller: messageListController,
itemBuilder: (context, index) {
var item = messageList[index];
return Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 10),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
item.keys.toList().toString(),
style: const TextStyle(
fontSize: 12, color: Colors.white),
),
const SizedBox(
width: 10,
),
Expanded(
child: Text(
item.values.toList()[0],
style: const TextStyle(
fontSize: 12, color: Colors.white),
),
)
],
),
);
},
itemCount: messageList.length,
),
),
Container(
height: 40,
color: Colors.black54,
padding: const EdgeInsets.only(left: 10),
child: Swiper(
itemBuilder: (context, index) {
return Container(
alignment: Alignment.centerLeft,
child: Text(
_joinTip[index],
style: const TextStyle(
color: Colors.white, fontSize: 14),
),
);
},
autoplayDelay: 1000,
physics: const NeverScrollableScrollPhysics(),
itemCount: _joinTip.length,
autoplay: true,
scrollDirection: Axis.vertical,
),
),
],
),
),
)
],
),
),
Container(
height: 80,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
child: Row(
children: [
Expanded(
child: TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
isDense: true,
),
controller: messageController,
keyboardType: TextInputType.number),
),
TextButton(
onPressed: () async {
if (isSpecialMessage(messageController.text) != true) {
messageList.add({widget.uid: messageController.text});
}
final data = Uint8List.fromList(
utf8.encode(messageController.text));
await _engine.sendStreamMessage(
streamId: streamId, data: data, length: data.length);
messageController.clear();
setState(() {});
// ignore: use_build_context_synchronously
FocusScope.of(context).requestFocus(FocusNode());
},
child: const Text("Send"))
],
),
),
],
),
);
}
}
从上面可以看到,其实跑完基础流程很简单,回顾一下前面的内容,总结下来就是:申请麦克风和摄像头权限
创建和通过App ID初始化引擎
注册RtcEngineEventHandler回调用于判断状态和接收互动能力
根绝角色打开和配置视频编码支持
调用joinChannel加入直播间
通过AgoraVideoView和VideoViewController用户画面
通过engine创建和发送stream消息
从申请账号到开发 Demo ,利用声网的 SDK 开发一个「互动直播」从需求到实现大概只过了一个小时,虽然上述实现的功能和效果还很粗糙,但是主体流程很快可以跑通了。同时在 Flutter 的加持下,代码可以在移动端和 PC 端得到复用,这对于有音视频需求的中小型团队来说无疑是最优组合之一。欢迎开发者们也尝试体验声网 SDK,实现实时音视频互动场景。现注册声网账号下载 SDK,可获得每月免费 10000 分钟使用额度。如在开发过程中遇到疑问,可在声网开发者社区与官方工程师交流。}

我要回帖

更多关于 聊天sdk 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信