前言
ChatGPT 最近一直都处于技术圈的讨论焦点。它除了可作为普通用户的日常 AI 助手,还可以帮助开发者加速开发进度。声网社区的一位开发者"小猿"就基于 ChatGPT 做了一场实验。仅 40 分钟就实现了一个互动直播 Demo。他是怎么做的呢?他将整个过程记录了下来。
(文章转载自开发者的个人博客,以下为正文)
“遇事不决,AI 力学” ~ ChatGPT 可以说是 2023 开年最热门的话题, 它不仅在极短时间内风靡了整个技术圈,更是病毒式地席卷了圈外的各个行业,并对各大企业都起到了实质性影响:
- 谷歌紧急推出 “Bard” 对抗 ChatGPT
- 微软发布新 Bing 集成 ChatGPT
- 复旦发布首个类 ChatGPT 模型 MOSS
- 国内阿里、百度、昆仑万维、网易、京东都开始新一轮 AI 军备
那 ChatGPT 究竟有什么魔力能让“群雄折腰”?这和 ChatGPT 的实现有很大关系:
与以往的统计模型不行,ChatGPT 不是那种「一切都从语料统计里学习」的 AI,相反 ChatGPT 具备有临场学习的能力,业内称之为 in-context learning ,这也是为什么 ChatGPT 可以在上下文中学习的原因。
ChatGPT 属于 AI 领域在商用技术上的重大突破,当然,本篇我们不是要讨论ChatGPT 的实现逻辑,而是 ChatGPT 会怎么样加速我们的开发?
PS:在此之前有人通过指示在 ChatGPT 界面下实现了一个虚拟机,虽然这是一个极端的例子,但是可以很直观地感受到:「ChatGPT 对我们开发的影响是肉眼可见」。
那 ChatGPT 在实际工作中是如何影响我们的开发?为了更直观,下面我们用一个开发场景来模拟这个流程。
基于 ChatGPT 开发
01 开发之前
假设我们现在有一个开发「直播」的需求,那我们可以直接求助 ChatGPT:
「开发一个直播app,是使用第三方SDK好还是自己从0开发好」?
如下图所示,从回答上可以看到,AI 建议我们根据团队实际情况去选择,而在知晓「我的团队只有 5 个人」的情况后,它建议我选择采用 “接入第三方 SDK” 的方式更合理。
那么选择 “接入第三方 SDK” ,接下来的问题就是:「选择做直播,在中国推荐使用哪些厂家的 SDK」?
如下图所示,这个问题 ChatGPT 同样提供了多个选项,从选项里看声网、腾讯云和阿里云好像都符合我们要求,而在接着的「优势问题」对比上看,这三个选项都“不相伯仲”,那我们就在再细化问题。
假设我们希望直播可以有更多“互动能力”,那么把问题修改为 「做互动直播,更推荐使用哪一个厂家的 SDK」 ,截图如下图所示,这次我们得到了更明确的答复,看来声网的 SDK 会更贴合我们的需求。
为了更放心这个选择,我们通过 「声网 SDK 的优势」 和 「什么产品使用了声网SDK」 两个问题进行提问,如下图所示,从回复上看声网作为一个全球化的厂家,在音视频领域还是值得相信。同时,还有包括小米、陌陌等产品都使用了声网的服务。那么就按照 AI 的建议选择声网 SDK 吧。
有没有发现,在获取资料的检索方式上,ChatGPT 确实比搜索引擎更直观且高效。
那么敲定完 SDK ,接下来我们需要选择应用的开发框架,我们把需求限定在 Android 和 iOS,更好是能兼容 Web,覆盖整个移动端 ,因为团队人数不多,所以我们希望采用跨平台开发来节约成本,那么问题就是:
「移动端哪个跨平台框架更适合做直播」?
如下图所示,得到的答案有 React Native 和 Flutter ,而恰好在 Flutter 回复里可以看到声网 SDK 的存在,所以我们可以敲定 App 开发框架就选 Flutter 了。
最后,在开发之前,我们还需要继续提问 「如何获取声网 SDK 」 和 「使用声网 SDK 需要做什么」,这样我们就可以在开始开发之前提前准备好需要的东西。
关于注册获取 App ID 等步骤这里就省略了,毕竟目前这部分 ChatGPT 也无能为力。
02 开始开发
那么到这里我们就假定大家已经准备好了开发环境,接下来可以直接进行开发。
我们还是继续面向 ChatGPT 开发,首先我们的提问是:「用声网的 Flutter SDK agora_rtc_engine 6.1.0 写一个视频通话页面,给我 dart 代码」 ,结果如下 GIF 所示,可以看到 ChatGPT 开始了疯狂的输出:
为什么关键词是「视频通话」?因为它比直播场景更精准简单,生成的代码更靠谱(经过提问测试),而基于视频通话部分,后面我们可以快速拓展为互动直播场景;而指定版本是为了避免
AI 使用旧版本 API。
从上门的代码生成可以看到,ChatGPT 生产的代码是自带中文注释,更贴心的是,如下图所示,在生成的代码末尾还给你解释了这段代码的实现逻辑,就像一个“知心大姐姐”。
从这里也可以感觉到 ,ChatGPT 不是一个单纯的完全只会基于语料答复整合的 AI 。
当然,直接复制生成的代码后会发现这段代码会报错,这和 ChatGPT 目前的模型数据版本有一定关系,所以针对生成的代码我们需要做一定手动调整,比如:
- 采用 createAgoraRtcEngine 和 initialize 创建和初始化 RtcEngine
- 将 setEventHandler 修改为最新的 registerEventHandler
- 将 AgoraRenderWidget 修改为 AgoraVideoView
最后修改代码如下,其中 80% 以上的逻辑都来自 ChatGPT 的自动生成,虽然没办法做到“直出”,这无疑大大提高了开发的生产力。
class VideoCallPage extends StatefulWidget {
final String channelName;
const VideoCallPage({Key? key, required this.channelName}) : super(key: key);
@override
_VideoCallPageState createState() => _VideoCallPageState();
}
class _VideoCallPageState extends State<VideoCallPage> {
late RtcEngine _engine;
bool _localUserJoined = false;
bool _remoteUserJoined = false;
int? rUid;
@override
void initState() {
super.initState();
initAgora();
}
@override
void dispose() {
_engine.leaveChannel();
super.dispose();
}
Future<void> initAgora() async {
await [Permission.microphone, Permission.camera].request();
_engine = createAgoraRtcEngine();
await _engine.initialize(RtcEngineContext(
appId: config.appId,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
));
_engine.registerEventHandler(RtcEngineEventHandler(
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
setState(() {
_localUserJoined = true;
});
},
onUserJoined: (connection, remoteUid, elapsed) {
setState(() {
_remoteUserJoined = true;
rUid = remoteUid;
});
},
onUserOffline: (RtcConnection connection, int remoteUid,
UserOfflineReasonType reason) {
setState(() {
_remoteUserJoined = false;
rUid = null;
});
},
));
await _engine.enableVideo();
await _engine.startPreview();
await _engine.joinChannel(
token: config.token,
channelId: widget.channelName,
uid: config.uid,
options: const ChannelMediaOptions(
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
clientRoleType: ClientRoleType.clientRoleBroadcaster,
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text("VideoCallPage"),),
body: Center(
child: Stack(
children: [
_remoteUserJoined ? _remoteVideoView(rUid) : _placeholderView(),
_localUserJoined ? _localVideoView() : _placeholderView(),
],
),
),
);
}
Widget _placeholderView() {
return Container(
color: Colors.black,
);
}
Widget _remoteVideoView(id) {
return AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: _engine,
canvas: VideoCanvas(uid: id),
connection: RtcConnection(channelId: widget.channelName),
),
);
}
Widget _localVideoView() {
return Positioned(
right: 16,
bottom: 16,
width: 100,
height: 160,
child: AgoraVideoView(
controller: VideoViewController(
rtcEngine: _engine,
canvas: const VideoCanvas(uid: 0),
),
),
);
}
}
接下来,如下图所示,在将项目运行到手机和 PC 端之后,可以看到我们就完成了最简单的直播视频场景,而基于我们打算做直播的念头仅仅过去了 40 分钟,这其中还包含了注册声网账号和申请 App ID 的过程,我们通过简单的提问、复制、粘贴、修改,就完成了一个直播需求的 demo。
红色方块是后期加上的打码~
那么到这里,虽然目前为止 demo 项目还不是互动直播,但是基于这个 demo 实现互动直播场景不会太难,因为你已经跑通了整个 SDK 的链路流程了。
03 进阶开发
那假设我们需要继续往互动直播的方向开发,那么我们肯定会遇到“互动”这个需求,比如「收到用户发送的一段内容后画面弹出一个动画」 这样的需求。
那么首先我们要知道声网 SDK 如何监听用户发送的内容,所以接下来我们继续提问:「如何使用声网的 agora_rtc_engine 6.1.0 监听别人发送的文本消息」 ?
这里为什么还强制写 agora_rtc_engine 6.1.0 ?因为如果不写,默认可能会输出 4.x 版本的老 API。
尽管得到的答案并不是 Dart 代码而是 OC ,但是关键词 registerEventHandler 和 Message 我们捕抓到了,简单对比一下,就是 Flutter SDK 里的 registerEventHandler 对象,可以发现平替的接口就是 onStreamMessage 回调。
那么接着就是弹出什么内容,因为我们没有素材,假设还没有设计师,那不如就让 ChatGPT 帮我们画一只兔子吧,不过测试结果并不好,如下图所示,从输出结果上看 ,这并不是我们想要的。
这里是我自己加的粉色,不然都是白色会糊成一坨,不得不说 ChatGPT 在绘制能力上“很抽象”。
所以 ChatGPT 有时候也不是很智能,可能目前在绘画理解上它还没那么成熟, 但是没问题, ChatGPT 是可以通过上下文学习“调教”的,比如我们觉得兔子的耳朵形状太离谱,那么我们可以让 ChatGPT 给我们调整。
如下所示,虽然调整之后依然不对,但是比起一开始是不是好很多了?
这就是 ChatGPT 在每次会话上下文里学习的表现。
然后我们在兔子耳朵的基础上再让 ChatGPT 补全兔子头,虽然最终的效果依然不理想,但是比起一开始已经进步了很多。
同时我们还让 ChatGPT 给我们画了一个“星星”,然后结合这两个 Canvas 绘制的素材,我们在代码里设置接收到 "兔子" 和 星星 文本的时候,就弹出一个放大动画效果。
最终运行后效果如下 GIF 所示,看起来很简陋,但是要知道,我们只是经过了简单的复制/粘贴就完成了这样的效果,这难道不是开发效率的极大提高?
源码在后面。
来自 ChatGPT 的兔子头代码:
class BunnyPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
// 设置画笔
final paint = Paint()
..color = Colors.white
..style = PaintingStyle.fill;
final path = Path();
// 绘制左耳
path.moveTo(size.width / 2 - 40, size.height / 2 - 80);
path.lineTo(size.width / 2 - 60, size.height / 2 - 120);
path.quadraticBezierTo(size.width / 2 - 70, size.height / 2 - 135,
size.width / 2 - 50, size.height / 2 - 160);
path.lineTo(size.width / 2 - 30, size.height / 2 - 120);
path.quadraticBezierTo(size.width / 2 - 40, size.height / 2 - 100,
size.width / 2 - 40, size.height / 2 - 80);
// 绘制路径
canvas.drawPath(path, paint);
final path2 = Path();
// 绘制右耳
path2.moveTo(size.width / 2 + 40, size.height / 2 - 80);
path2.lineTo(size.width / 2 + 60, size.height / 2 - 120);
path2.quadraticBezierTo(size.width / 2 + 70, size.height / 2 - 135,
size.width / 2 + 50, size.height / 2 - 160);
path2.lineTo(size.width / 2 + 30, size.height / 2 - 120);
path2.quadraticBezierTo(size.width / 2 + 40, size.height / 2 - 100,
size.width / 2 + 40, size.height / 2 - 80);
// 绘制路径
canvas.drawPath(path2, paint);
final path3 = Path();
// 绘制头部
final rect =
Rect.fromLTWH(size.width / 2 - 60, size.height / 2 - 140, 120, 120);
path3.addOval(rect);
// 绘制路径
canvas.drawPath(path3, paint);
final path4 = Path();
// 绘制眼睛
final leftEyeCenter = Offset(size.width / 2 - 20, size.height / 2 - 80);
final rightEyeCenter = Offset(size.width / 2 + 20, size.height / 2 - 80);
final eyeRadius = 8.0;
path4.addArc(
Rect.fromCircle(center: leftEyeCenter, radius: eyeRadius), 0, pi * 2);
path4.addArc(
Rect.fromCircle(center: rightEyeCenter, radius: eyeRadius), 0, pi * 2);
// 设置画笔
final paint2 = Paint()
..color = Colors.black
..style = PaintingStyle.fill;
// 绘制路径
canvas.drawPath(path4, paint2);
final path5 = Path();
// 绘制鼻子
final noseCenter = Offset(size.width / 2, size.height / 2 - 50);
final noseRadius = 10.0;
path5.addArc(
Rect.fromCircle(center: noseCenter, radius: noseRadius), 0, pi * 2);
// 绘制路径
canvas.drawPath(path5, paint2);
}
@override
bool shouldRepaint(BunnyPainter oldDelegate) => false;
}
来自 ChatGPT 的星星代码:
class StarPaint extends StatelessWidget {
@override
Widget build(BuildContext context) {
return CustomPaint(
painter: HeartPainter(),
size: Size(50, 50),
);
}
}
class StarPaint extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
final paint = Paint()
..color = Colors.red
..style = PaintingStyle.fill;
final path = Path();
final halfWidth = size.width / 2;
final halfHeight = size.height / 2;
final radius = halfWidth;
path.moveTo(halfWidth, halfHeight + radius);
path.arcToPoint(
Offset(halfWidth + radius, halfHeight),
radius: Radius.circular(radius),
clockwise: true,
);
path.arcToPoint(
Offset(halfWidth, halfHeight - radius),
radius: Radius.circular(radius),
clockwise: true,
);
path.arcToPoint(
Offset(halfWidth - radius, halfHeight),
radius: Radius.circular(radius),
clockwise: true,
);
path.arcToPoint(
Offset(halfWidth, halfHeight + radius),
radius: Radius.circular(radius),
clockwise: true,
);
canvas.drawPath(path, paint);
}
@override
bool shouldRepaint(covariant StarPaint oldDelegate) {
return false;
}
}
自己补充的监听文本、发送文本和动画效果代码:
onStreamMessage: (RtcConnection connection, int remoteUid, int streamId,
Uint8List data, int length, int sentTs) {
var message = utf8.decode(data);
if (message == "兔子") {
showDialog(
context: context,
builder: (context) {
return AnimaWidget(Rabbit());
});
} else if (message == "星星") {
showDialog(
context: context,
builder: (context) {
return Center(
child: AnimaWidget(StarPaint()),
);
});
}
Future.delayed(Duration(seconds: 3), () {
Navigator.pop(context);
});
},
Future<void> _onPressSend() async {
try {
final streamId = await _engine.createDataStream(
const DataStreamConfig(syncWithAudio: false, ordered: false));
var txt = (Random().nextInt(10) % 2 == 0) ? "星星" : "兔子";
final data = Uint8List.fromList(utf8.encode(txt));
await _engine.sendStreamMessage(
streamId: streamId, data: data, length: data.length);
} catch (e) {
print(e);
}
}
class AnimaWidget extends StatefulWidget {
final Widget child;
const AnimaWidget(this.child);
@override
State<AnimaWidget> createState() => _AnimaWidgetState();
}
class _AnimaWidgetState extends State<AnimaWidget> {
double animaScale = 1;
@override
void initState() {
super.initState();
Future.delayed(Duration(seconds: 1), () {
animaScale = 5;
setState(() {});
});
}
@override
Widget build(BuildContext context) {
return AnimatedScale(
scale: animaScale,
duration: Duration(seconds: 1),
curve: Curves.bounceIn,
child: Container(child: widget.child));
}
}
相信到这里大家应该可以感受到 ChatGPT 提高开发效率的魅力,甚至你还可以把 ChatGPT 集成到你的直播场景里,通过 Flutter 上的 chatgpt_api_client 插件,你可以在 App 里直接向 ChatGPT 提问,比如通过 OpenAI 的 API 实现一个可以互动的虚拟主播。
我怎么知道这个插件?肯定也是问 ChatGPT 的啊~
04 最后
到这里,相信大家应该能感受到,在使用 ChatGPT 之后,整个开发效率能够得到很大的提升,特别是内容检索的高效和准确上比搜索引擎更加靠谱,另外也能帮我们完成一些“体力活”形式的代码。
当然我们也看到了目前 ChatGPT 并不能完全替代人工,因为它在很多方面生成的内容并不完美,特别是很多代码还是需要我们人工调整,但是这并不影响 ChatGPT 的价值。
最后引用我曾经看到过的关于 ChatGPT 的一些评价:
「当你抱怨 ChatGPT鬼话连篇满嘴跑火车的时候,这可能有点像你看到一只猴子在沙滩上用石头写下1+1=3。它确实算错了,但这不是重点。它有一天会算对的。」
我相信 AI 并不是直接取代人类的方式,因为它对社会的挤压不是从水平上碾压,而是劣币驱逐良币,比如有位大佬就说过:「乙方最讨厌甲方什么都不懂还bb,但乙方的议价权恰恰来源于甲方什么都不懂还 bb」 ,而现在 ChatGPT 在慢慢消磨掉整个议价权。
总的来说「ChatGPT 只是一个产品,它不代表的整个技术的“上限” ,它代表的是技术已经到达商用的临界点」。
现在,它在慢慢成为开发圈子里的习惯,和曾经的 Copilot 一样,而同时它在其他领域如文字编排等的能力,甚至远超它在开发领域的价值。
欢迎开发者们也尝试体验声网 SDK,实现实时音视频互动场景。现注册声网账号下载 SDK,可获得每月免费 10000 分钟使用额度。如在开发过程中遇到疑问,可在声网开发者社区与官方工程师交流。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。