現在はReact Nativeでビデオ通信を用いたアプリを開発しています。
初期の開発はReact Native WebRTCを使用していましたが、現在はドキュメントの充実と将来的な開発の安定性を考えてReact Native Agoraを使用しています。
導入時期はまだv3系でしたが、node_modulesをアップデートした際にv4系に上がっていました。
修正する箇所が何箇所かあったので、まずは調査を兼ねてミニマムなものを作りました。
作ったもの
以下の要件のシンプルなものを作成しました。
- スクリーンをロードした際に自分のカメラの映像を表示
- 「JOIN」をタップするとserverへuidTokenを作成をリクエストし、チャンネルへJOINする
- 両方のデバイスがJOINすると相手の画面に映像が小窓で表示される
- 「LEAVE」をタップすると映像が切れる
コードの変更点
engineの作成
v3はcreateWithContext
でPromiseが返ってきましたが、v4ではその辺りは意識しなくてよくなっています。
// v3
const engine = await RtcEngine.createWithContext(new RtcEngineContext(appId))
// v4
const engine = createAgoraRtcEngine()
engine.initialize({ appId, channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting })
Eventの作成
イベント名は「on + イベント名」に変更されています。
// v3
engine.addListener('JoinChannelSuccess', joinSuccessHandler)
engine.addListener('Error', (e) => console.info(e))
engine.addListener('UserJoined', userJoinedHandler)
engine.addListener('UserOffline', userOfflineHandler)
engine.addListener('LeaveChannel', leaveChannelHandler)
// v4
engine.addListener('onJoinChannelSuccess', joinSuccessHandler)
engine.addListener('onError', (e) => console.info(e))
engine.addListener('onUserJoined', userJoinedHandler)
engine.addListener('onUserOffline', userOfflineHandler)
engine.addListener('onLeaveChannel', leaveChannelHandler)
joinChannelの引数
v3の第3引数がoptionalInfo
ということですが、型を見ると"optionalInfo (Optional) Reserved for future use." となっており、おそらく使われないままv4に移行したのかなと思います。
v4ではv3で第4引数だったuidが第3引数になっており、第4引数ではChannelMediaOptions
を指定するようになっています。
// v3
engine.joinChannel(token, 'CHANNEL_ID', null, uid)
// v4
engine.joinChannel(token, 'CHANNEL_ID', uid, {
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
}
)
RtcLocalViewとRtcRemoteView
v3で使用していたRtcLocalViewとRtcRemoteViewはなくなりました。
RtcLocalViewがなくなったため、canvasの{ uid }
で誰の映像かを判定しているのかと思います。ちょっとドキュメントでは見つけられなかったのですが、どうやら0を指定すると自分のカメラの映像がプレビューされるっぽいです。
// v3
const memberView = useMemo(() => {
return members.map((uid, key) => {
return (
<RtcRemoteView.SurfaceView
key={key}
style={styles.memberView}
uid={uid}
zOrderMediaOverlay={true}
/>
)
})
}, [members])
return (
<View>
<RtcLocalView.SurfaceView style={styles.localView} />
{memberView}
</View>
)
// v4
const memberView = useMemo(() => {
return members.map((uid, key) => {
return (
<RtcSurfaceView
key={key}
style={styles.memberView}
zOrderMediaOverlay={true}
canvas={{ uid }}
/>
)
})
}, [members])
return (
<View>
<RtcSurfaceView canvas={{ uid: 0 }} style={styles.localView} />
{memberView}
</View>
)
v4のテストに使用したコード
tokenの生成にserver.jsを使用しています。
MainScreen.tsx
import React, { useCallback, useEffect, useMemo, useState } from 'react'
import { StyleSheet, Text, Button, View } from 'react-native'
import { createAgoraRtcEngine, RtcSurfaceView, ChannelProfileType, ClientRoleType } from 'react-native-agora'
import type { IRtcEngine, RtcConnection, UserOfflineReasonType, RtcStats } from 'react-native-agora'
import { agora } from '../../constants'
export default function MainScreen() {
const [engine, setEngine] = useState<IRtcEngine>()
const [uids, setUids] = useState<number[]>([])
const [id] = useState<number>(() => Math.floor(Math.random() * 1000))
useEffect(() => {
const mount = async () => {
if (engine) return
const rtcEngine = createAgoraRtcEngine()
rtcEngine.initialize({ appId: agora.appId, channelProfile: ChannelProfileType.ChannelProfileLiveBroadcasting })
rtcEngine.addListener('onJoinChannelSuccess', joinSuccessHandler)
rtcEngine.addListener('onError', (e) => console.info(e))
rtcEngine.addListener('onUserJoined', userJoinedHandler)
rtcEngine.addListener('onUserOffline', userOfflineHandler)
rtcEngine.addListener('onLeaveChannel', leaveChannelHandler)
rtcEngine.enableVideo()
rtcEngine.disableAudio()
rtcEngine.startPreview()
setEngine(rtcEngine)
}
mount()
}, [engine])
const joinSuccessHandler = (connection: RtcConnection, elapsed: number) => {
console.info('JoinChannelSuccess', connection, elapsed)
}
const userJoinedHandler = (connection: RtcConnection, remoteUid: number, elapsed: number) => {
console.info('UserJoined', connection, remoteUid, elapsed)
setUids((items) => [...items, remoteUid])
}
const userOfflineHandler = (connection: RtcConnection, remoteUid: number, reason: UserOfflineReasonType) => {
console.info('UserOffline', connection, remoteUid, reason)
setUids((items) => items.filter((item) => item !== remoteUid))
}
const leaveChannelHandler = (connection: RtcConnection, stats: RtcStats) => {
console.info('LeaveChannel', connection, stats)
setUids([])
}
const _handleJoinChannel = useCallback(async () => {
if (!engine) return
const res = await fetch('http://xxxxxxxxxxxxx/token/create', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify({ id, channel: agora.channelId })
})
.then((res) => res.json())
.catch((error) => {
console.error('エラー', error)
})
engine.joinChannel(
res.uidToken, agora.channelId, id, {
clientRoleType: ClientRoleType.ClientRoleBroadcaster,
}
)
}, [engine])
const _handleLeaveChannel = useCallback(async () => {
if (!engine) return
engine.leaveChannel()
}, [engine])
const members = useMemo(() => {
if (!uids.length) return
return uids.map((uid, key) => {
return (
<RtcSurfaceView
key={key}
style={styles.remote}
zOrderMediaOverlay={uid !== 0}
canvas={{ uid }}
/>
)
})
}, [uids])
return (
<View style={styles.container}>
<View>
<Text style={styles.title}>MainScreen #{id}</Text>
<Button
onPress={_handleJoinChannel}
title={'JOIN'}
/>
<Button
onPress={_handleLeaveChannel}
title={'LEAVE'}
/>
</View>
<View>
<RtcSurfaceView canvas={{ uid: 0 }} style={styles.local} />
{members}
</View>
</View>
)
}
const styles = StyleSheet.create({
container: {
flex: 1,
},
title: {
fontSize: 20,
fontWeight: 'bold',
},
local: {
width: '100%',
height: '100%'
},
remote: {
position: 'absolute',
top: 20,
right: 20,
width: 120,
height: 180
},
})
server.js
const express = require("express")
const { RtcTokenBuilder, RtcRole } = require('agora-access-token')
const constants = {
appId: "xxx",
appCertificate: "xxx"
}
const app = express()
const PORT = 3000
app.use(express.json())
app.use(express.urlencoded({ extended: true }))
app.post("/token/create", (req, res) => {
const channelName = req.body.channel
const uid = req.body.id
const expirationTimeInSeconds = 36000
const currentTimestamp = Math.floor(Date.now() / 1000)
const privilegeExpiredTs = currentTimestamp + expirationTimeInSeconds
const uidToken = RtcTokenBuilder.buildTokenWithUid(
constants.appId,
constants.appCertificate,
channelName,
uid,
RtcRole.PUBLISHER,
privilegeExpiredTs
)
return res.status(200).json({
uidToken,
})
})
app.listen(PORT, () => {
console.log(`Example app listening on port ${PORT}`)
})
気になったところ
switchCamera()がロード時にうまく動かない
おそらくv3の時はいろいろな処理がPromiseの非同期処理が完了してから、switchCameraを呼んでいたので切り替えれたのかと思います。
今回の場合は特にPromiseが返ってくるものがない為、startPreview()
する前にdeleyした方がいいのかもしれません。(未実施)
そもそもですが「使用するカメラを指定させて欲しい」「現状のカメラの状態を確認する値が欲しい」というのがあり、それができればこの問題も根本的に解決できるとは思います。
まとめ
個人的にはv3に不満があったわけではありませんが、これからの開発とメンテのことを考えコードもまだ小さいこともありv4に移行していく予定です。
React Nativeを用いたAgoraの事例がまだまだ少ないので、今後もっと増えていくといいなーと思っています。