LoginSignup
0

More than 1 year has passed since last update.

React Native Agora v3からv4の変更点

Last updated at Posted at 2022-12-24

現在はReact Nativeでビデオ通信を用いたアプリを開発しています。

初期の開発はReact Native WebRTCを使用していましたが、現在はドキュメントの充実と将来的な開発の安定性を考えてReact Native Agoraを使用しています。

導入時期はまだv3系でしたが、node_modulesをアップデートした際にv4系に上がっていました。

修正する箇所が何箇所かあったので、まずは調査を兼ねてミニマムなものを作りました。

作ったもの

以下の要件のシンプルなものを作成しました。

  1. スクリーンをロードした際に自分のカメラの映像を表示
  2. 「JOIN」をタップするとserverへuidTokenを作成をリクエストし、チャンネルへJOINする
  3. 両方のデバイスがJOINすると相手の画面に映像が小窓で表示される
  4. 「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の事例がまだまだ少ないので、今後もっと増えていくといいなーと思っています。

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0