Must Have 코드팩토리의 플러터 프로그래밍 - 12 ~ 13
book
Must Have 코드팩토리의 플러터 프로그래밍 12 ~ 13
12장 [Project] 동영상 플레이어
P.303 (12.1.3)
시간 변환 및 String 패딩
void main() {
Duration duration = Duration(seconds: 192);
var timeCase1 = duration;
var timeCase2 = duration.toString().split('.')[0];
var timeCase3 = duration.toString().split('.')[0].split(':').sublist(1, 3).join(':');
var timeCase4 = '${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}';
print(timeCase1); // 0:03:12.000000
print(timeCase2); // 0:03:12
print(timeCase3); // 03:12
print(timeCase4); // 03:12
}
P.340 (12.5)
테스트하기 : 최종 완성본 결과
// lib/main.dart
import 'package:vid_player/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
);
}
// lib/screen/home_screen.dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:vid_player/component/custom_video_player.dart';
class HomeScreen extends StatefulWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
State<HomeScreen> createState() => _HomeScreenState();
}
class _HomeScreenState extends State<HomeScreen> {
XFile? video;
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: video == null ? renderEmpty() : renderVideo(),
);
}
Widget renderEmpty(){
return Container(
width: MediaQuery.of(context).size.width,
decoration: getBoxDecoration(),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_Logo(
onTap: onNewVideoPressed,
),
SizedBox(height: 30.0),
_AppName(),
],
),
);
}
void onNewVideoPressed() async {
final video = await ImagePicker().pickVideo(
source: ImageSource.gallery,
);
if (video != null) {
setState(() {
this.video = video;
});
}
}
BoxDecoration getBoxDecoration() {
return BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xFF2A3A7C),
Color(0xFF000118),
],
),
);
}
Widget renderVideo() {
return Center(
child: CustomVideoPlayer(
video: video!,
onNewVideoPressed: onNewVideoPressed,
),
);
}
}
class _Logo extends StatelessWidget {
final GestureTapCallback onTap;
const _Logo({
required this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap,
child: Image.asset(
'asset/img/logo.png',
),
);
}
}
class _AppName extends StatelessWidget {
const _AppName({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
final textStyle = TextStyle(
color: Colors.white,
fontSize: 30.0,
fontWeight: FontWeight.w300,
);
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
'VIDEO',
style: textStyle,
),
Text(
'PLAYER',
style: textStyle.copyWith(
fontWeight: FontWeight.w700,
),
),
],
);
}
}
// lib/component/custom_icon_button.dart
import 'package:flutter/material.dart';
class CustomIconButton extends StatelessWidget {
final GestureTapCallback onPressed;
final IconData iconData;
const CustomIconButton({
required this.onPressed,
required this.iconData,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return IconButton(
onPressed: onPressed,
iconSize: 30.0,
color: Colors.white,
icon: Icon(
iconData,
),
);
}
}
// lib/component/custom_video_player.dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_player/video_player.dart';
import 'dart:io';
import 'package:vid_player/component/custom_icon_button.dart';
class CustomVideoPlayer extends StatefulWidget {
final XFile video;
final GestureTapCallback onNewVideoPressed;
const CustomVideoPlayer({
required this.video,
required this.onNewVideoPressed,
Key? key,
}) : super(key: key);
@override
State<CustomVideoPlayer> createState() => _CustomVideoPlayerState();
}
class _CustomVideoPlayerState extends State<CustomVideoPlayer> {
VideoPlayerController? videoController;
bool showControls = false;
@override
void didUpdateWidget(covariant CustomVideoPlayer oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.video.path != widget.video.path) {
initializeController();
}
}
@override
void initState() {
super.initState();
initializeController();
}
initializeController() async {
final videoController = VideoPlayerController.file(
File(widget.video.path),
);
await videoController.initialize();
videoController.addListener(videoControllerListener);
setState(() {
this.videoController = videoController;
});
}
void videoControllerListener() {
setState(() {});
}
@override
void dispose() {
videoController?.removeListener(videoControllerListener);
super.dispose();
}
@override
Widget build(BuildContext context) {
if (videoController == null) {
return Center(
child: CircularProgressIndicator(),
);
}
return GestureDetector(
onTap: () {
setState(() {
showControls = !showControls;
});
},
child: AspectRatio(
aspectRatio: videoController!.value.aspectRatio,
child: Stack(
children: [
VideoPlayer(
videoController!,
),
if(showControls)
Container(
color: Colors.black.withOpacity(0.5),
),
Positioned(
bottom: 0,
left: 0,
right: 0,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
renderTimeTextFromDuration(
videoController!.value.position,
),
Expanded(
child: Slider(
onChanged: (double val) {
videoController!.seekTo(
Duration(seconds: val.toInt()),
);
},
value: videoController!.value.position.inSeconds
.toDouble(),
min: 0,
max: videoController!.value.duration.inSeconds
.toDouble(),
),
),
renderTimeTextFromDuration(
videoController!.value.duration,
),
],
),
),
),
if(showControls)
Align(
alignment: Alignment.topRight,
child: CustomIconButton(
onPressed: widget.onNewVideoPressed,
iconData: Icons.photo_camera_back,
),
),
if (showControls)
Align(
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
CustomIconButton(
onPressed: onReversePressed,
iconData: Icons.rotate_left,
),
CustomIconButton(
onPressed: onPlayPressed,
iconData: videoController!.value.isPlaying
? Icons.pause
: Icons.play_arrow,
),
CustomIconButton(
onPressed: onForwardPressed,
iconData: Icons.rotate_right,
),
],
),
),
],
),
),
);
}
Widget renderTimeTextFromDuration(Duration duration) {
return Text(
'${duration.inMinutes.toString().padLeft(2, '0')}:${(duration.inSeconds % 60).toString().padLeft(2, '0')}',
style: TextStyle(
color: Colors.white,
),
);
}
void onReversePressed() {
final currentPosition = videoController!.value.position;
Duration position = Duration();
if (currentPosition.inSeconds > 3) {
position = currentPosition - Duration(seconds: 3);
}
videoController!.seekTo(position);
}
void onForwardPressed() {
final maxPosition = videoController!.value.duration;
final currentPosition = videoController!.value.position;
Duration position = maxPosition;
if ((maxPosition - Duration(seconds: 3)).inSeconds >
currentPosition.inSeconds) {
position = currentPosition + Duration(seconds: 3);
}
videoController!.seekTo(position);
}
void onPlayPressed() {
if (videoController!.value.isPlaying) {
videoController!.pause();
} else {
videoController!.play();
}
}
}
13장 [Project] 영상 통화
P.379 (13.5)
테스트하기 : 최종 완성본 결과
// lib/main.dart
import 'package:video_call/screen/home_screen.dart';
import 'package:flutter/material.dart';
void main() {
runApp(
MaterialApp(
debugShowCheckedModeBanner: false,
home: HomeScreen(),
),
);
}
// lib/screen/cam_screen.dart
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/const/agora.dart';
class CamScreen extends StatefulWidget {
const CamScreen({Key? key}) : super(key: key);
@override
_CamScreenState createState() => _CamScreenState();
}
class _CamScreenState extends State<CamScreen> {
RtcEngine? engine;
int? uid;
int? otherUid;
Future<bool> init() async {
final resp = await [Permission.camera, Permission.microphone].request();
final cameraPermission = resp[Permission.camera];
final micPermission = resp[Permission.microphone];
if (cameraPermission != PermissionStatus.granted ||
micPermission != PermissionStatus.granted) {
throw '카메라 또는 마이크 권한이 없습니다.';
}
if (engine == null) {
engine = createAgoraRtcEngine();
await engine!.initialize(
RtcEngineContext(
appId: APP_ID,
channelProfile: ChannelProfileType.channelProfileLiveBroadcasting,
),
);
engine!.registerEventHandler(
RtcEngineEventHandler(
onJoinChannelSuccess: (RtcConnection connection, int elapsed) {
print('채널에 입장했습니다. uid : ${connection.localUid}');
setState(() {
this.uid = connection.localUid;
});
},
onLeaveChannel: (RtcConnection connection, RtcStats stats) {
print('채널 퇴장');
setState(() {
uid = null;
});
},
onUserJoined: (RtcConnection connection, int remoteUid, int elapsed) {
print('상대가 채널에 입장했습니다. uid : $remoteUid');
setState(() {
otherUid = remoteUid;
});
},
onUserOffline: (RtcConnection connection, int remoteUid,
UserOfflineReasonType reason) {
print('상대가 채널에서 나갔습니다. uid : $uid');
setState(() {
otherUid = null;
});
},
),
);
await engine!.setClientRole(role: ClientRoleType.clientRoleBroadcaster);
await engine!.enableVideo();
await engine!.startPreview();
await engine!.joinChannel(
token: TEMP_TOKEN,
channelId: CHANNEL_NAME,
options: ChannelMediaOptions(),
uid: 0,
);
}
return true;
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('LIVE'),
),
body: FutureBuilder(
future: init(),
builder: (BuildContext context, AsyncSnapshot 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: EdgeInsets.symmetric(horizontal: 8.0),
child: ElevatedButton(
onPressed: () async {
if (engine != null) {
await engine!.leaveChannel();
}
Navigator.of(context).pop();
},
child: Text('채널 나가기'),
),
),
],
);
},
),
);
}
Widget renderSubView() {
if (uid != null) {
return AgoraVideoView(
controller: VideoViewController(
rtcEngine: engine!,
canvas: const VideoCanvas(uid: 0),
),
);
} else {
return CircularProgressIndicator();
}
}
Widget renderMainView() {
if (otherUid != null) {
return AgoraVideoView(
controller: VideoViewController.remote(
rtcEngine: engine!,
canvas: VideoCanvas(uid: otherUid),
connection: const RtcConnection(channelId: CHANNEL_NAME),
),
);
} else {
return Center(
child: const Text(
'다른 사용자가 입장할 때까지 대기해주세요.',
textAlign: TextAlign.center,
),
);
}
}
}
// lib/screen/home_screen.dart
import 'package:flutter/material.dart';
import 'package:video_call/screen/cam_screen.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.blue[100]!,
body: SafeArea(
child: Padding(
padding: const EdgeInsets.all(8.0),
child: Column(
children: [
Expanded(child: _Logo()),
Expanded(child: _Image()),
Expanded(child: _EntryButton()),
],
),
),
),
);
}
}
class _Logo extends StatelessWidget {
const _Logo({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(16.0),
boxShadow: [
BoxShadow(
color: Colors.blue[300]!,
blurRadius: 12.0,
spreadRadius: 2.0,
),
],
),
child: Padding(
padding: EdgeInsets.all(16.0),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(
Icons.videocam,
color: Colors.white,
size: 40.0,
),
SizedBox(width: 12.0),
Text(
'LIVE',
style: TextStyle(
color: Colors.white,
fontSize: 30.0,
letterSpacing: 4.0,
),
),
],
),
),
),
);
}
}
class _Image extends StatelessWidget {
const _Image({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Center(
child: Image.asset(
'asset/img/home_img.png',
),
);
}
}
class _EntryButton extends StatelessWidget {
const _EntryButton({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Column(
mainAxisAlignment: MainAxisAlignment.end,
crossAxisAlignment: CrossAxisAlignment.stretch,
children: [
ElevatedButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(
builder: (_) => CamScreen(),
),
);
},
child: Text('입장하기'),
),
],
);
}
}
// lib/const/agora.dart
const APP_ID = '앱ID를 입력해주세요!!!';
const CHANNEL_NAME = '채널 이름을 입력해주세요!!!';
const TEMP_TOKEN = '토큰값을 입력해주세요!!!';
참고
이 글은 골든래빗 《Must Have 코드팩토리의 플러터 프로그래밍 2판》의 스터디 내용 입니다.
스터디
Q.
A.