- 주요 스택
- Agora API
- RTC (Real Time Communication)
- 버튼 쉐도잉
- Stack
- permission_handler plugin (모바일 기기의 모든 퍼미션을 해당 플러그인을 통해 요청하고 받는 것이 가능)
permission_handler 를 이용하여 권한 받기 구현
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('LIVE'),
),
body: FutureBuilder<bool>(
future: init(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toString(),
));
}
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(), // 에러가 없고, 데이터가 없으면 아직 승인 대기중
);
}
return Column(
children: [],
);
}),
);
Future<bool> init() async {
final resp = await [Permission.camera, Permission.microphone]
.request(); // 권한 요청 (permission_handler 사용)
final cameraPermission = resp[Permission.camera]; // 카메라 권한 요청 결과값
final microphonePermission = resp[Permission.microphone]; // 마이크 권한 요청 결과값
/*
* denied = 아직 권한 요청 전 상태
* granted = 권한 승인
* restricted = ios 주로 사용하고, 부분적 권한
* limited = 사용자가 직접 몇가지 권한만 허용해주는 경우
* permanentlyDenied = 권한 거절 (이 경우, 다시 허용하려면 사용자가 직접 세팅으로 가서 해당 권한을 열어야함)
* */
if (cameraPermission != PermissionStatus.granted ||
microphonePermission != PermissionStatus.granted) {
throw '카메라 또는 마이크 권한이 없습니다.';
}
return true;
}
Agora API, RTC engine
import 'package:agora_rtc_engine/agora_rtc_engine.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:video_call_test/const/agora.dart';
class CamScreen extends StatefulWidget {
const CamScreen({super.key});
@override
State<CamScreen> createState() => _CamScreenState();
}
class _CamScreenState extends State<CamScreen> {
RtcEngine? engine;
// 내 ID
int? uid = 0;
// 상대 ID
int? otherUid;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('LIVE'),
),
body: FutureBuilder<bool>(
future: init(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Text(
snapshot.error.toString(),
));
}
if (!snapshot.hasData) {
return Center(
child: CircularProgressIndicator(), // 에러가 없고, 데이터가 없으면 아직 승인 대기중
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
Expanded(
child: Stack(
children: [
renderMainView(),
Align(
alignment: Alignment.topLeft,
child: Container(
color: Colors.grey,
height: 160,
width: 120,
child: renderSubView(),
),
),
],
),
),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: ElevatedButton(
onPressed: () async {
if (engine != null) {
await engine!.leaveChannel();
engine = null;
}
Navigator.of(context).pop();
},
child: Text('채널 나가기'),
),
)
],
);
},
),
);
}
void dispose() async {
if (engine != null) {
await engine!.leaveChannel(
options: LeaveChannelOptions(),
);
engine!.release();
}
super.dispose();
}
renderMainView() {
if (uid == null) {
return Center(
child: Text('채널에 참여해주세요.'),
);
} else {
// 채널에 참여하고 있을 때
return AgoraVideoView(
controller: VideoViewController(
rtcEngine: engine!,
canvas: VideoCanvas(
uid: 0,
),
),
);
}
}
renderSubView() {
if (otherUid == null) {
return Center(
child: Text('채널에 유저가 없습니다.'),
);
} else {
return AgoraVideoView(
controller: VideoViewController.remote(
// 상대방 보여줄때
rtcEngine: engine!,
canvas: VideoCanvas(uid: otherUid),
connection: RtcConnection(channelId: CHANNEL_NAME),
),
);
}
}
Future<bool> init() async {
final resp = await [Permission.camera, Permission.microphone]
.request(); // 권한 요청 (permission_handler 사용)
final cameraPermission = resp[Permission.camera]; // 카메라 권한 요청 결과값
final microphonePermission = resp[Permission.microphone]; // 마이크 권한 요청 결과값
/*
* denied = 아직 권한 요청 전 상태
* granted = 권한 승인
* restricted = ios 주로 사용하고, 부분적 권한
* limited = 사용자가 직접 몇가지 권한만 허용해주는 경우
* permanentlyDenied = 권한 거절 (이 경우, 다시 허용하려면 사용자가 직접 세팅으로 가서 해당 권한을 열어야함)
* */
if (cameraPermission != PermissionStatus.granted ||
microphonePermission != PermissionStatus.granted) {
throw '카메라 또는 마이크 권한이 없습니다.';
}
if (engine == null) {
engine = createAgoraRtcEngine();
await engine!.initialize(
RtcEngineContext(
appId: APP_ID,
),
);
engine!.registerEventHandler(
RtcEngineEventHandler(
// connection => 연결 정보
// elapsed => 연결된 시간 (연결된지 얼마나 됐는지)
// 내가 채널에 입장했을때
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
print('채널에 입장했습니다. uid: ${connection.localUid}');
setState(() {
uid = connection.localUid;
});
},
// 내가 채널을 나갔을때
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
print('채널 퇴장');
setState(() {
uid = null;
});
},
// 상대방 유저가 들어왔을때
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
print('상대가 채널에 입장했습니다. otherUid: ${remoteUid}');
setState(() {
otherUid = remoteUid;
});
},
// 상대가 나갔을때
onUserOffline: (RtcConnection connection, int remoteUid,
UserOfflineReasonType reason) {
print('상대가 채널에서 나갔습니다. otherUid : $remoteUid');
setState(() {
otherUid = null;
});
},
),
);
await engine!.enableVideo(); // 카메라 활성화
await engine!.startPreview(); // 카메라로 찍고 있는 모습을 휴대폰으로 송출
ChannelMediaOptions options = ChannelMediaOptions();
await engine!.joinChannel(
token: TEMP_TOKEN,
channelId: CHANNEL_NAME,
uid: 0,
options: options,
);
}
return true;
}
}