스마트시대
19 VIDEO RECORDING Introduction 19.1 Installation 19.2 CameraController 19.3 Selfie Mode 19.4 Flash Mode 19.5 Recording Animation 19.6 startVideoRecording 19.7 GallerySaver 19.8 ImagePicker 19.9 AppLifecycleState 본문
19 VIDEO RECORDING Introduction 19.1 Installation 19.2 CameraController 19.3 Selfie Mode 19.4 Flash Mode 19.5 Recording Animation 19.6 startVideoRecording 19.7 GallerySaver 19.8 ImagePicker 19.9 AppLifecycleState
스마트시대 2023. 5. 24. 23:51iPhone으로 하고 싶으면 여기보고 하기
https://docs.flutter.dev/get-started/install/macos
Deploy to iOS devices
deverloper mode on 하고
이거 실행

아이디 추가하고 bundle 채우기

이 상태가 되면 재생(플레이)버튼 누르기

이에러 나면 Settings -> General -> Device Management and "trust" 이거도 해줘야 함(소스 어플에 대한 허가)
다른 맥북에서 할 경우
밑의 경우 체크
1.
Failed to create provisioning profile. There are no devices registered in your account on the developer website. Select a device run destination to have Xcode register it.

주소창에 내 장비가 제대로 표시되어 있는지 확인

재기동해야될 경우도

2.아이디는 한 PC당 한 개밖에 안되니 똑같은 아이디로 시도
3.iproxy는 플러터 관련 파일이니 지우지 말고 허가 해줘


4. vscode 등 다 끄고 flutter clean, run시도
5.이거는 한 번 지정하면 바꾸지말자

Android setup
19.1 Installation



https://pub.dev/packages/camera
Android

이런 상태면 밑에서 다운 받고

flutter doctor --android-licenses
실행



처음 실행할 때는 오래걸린다.
19.2 CameraController

import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
class VideoRecordingScreen extends StatefulWidget {
const VideoRecordingScreen({super.key});
@override
State<VideoRecordingScreen> createState() => _VideoRecordingScreenState();
}
class _VideoRecordingScreenState extends State<VideoRecordingScreen> {
//flag
bool _hasPermission = false;
Future<void> initPermission() async {
final cameraPermission = await Permission.camera.request();
final micPermission = await Permission.microphone.request();
final cameraDenied =
cameraPermission.isDenied || cameraPermission.isPermanentlyDenied;
final micDenied =
micPermission.isDenied || micPermission.isPermanentlyDenied;
if (!cameraDenied && !micDenied) {
_hasPermission = true;
setState(() {});
}
}
@override
void initState() {
super.initState();
initPermission();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Container(),
);
}
}

bool _hasPermission = false;
---------------
// 카메라 초기화 메소드
Future<void> initCamera() async {
final cameras = await availableCameras();
print(cameras);
}
---------------
if (!cameraDenied && !micDenied) {
_hasPermission = true;
---------------
await initCamera();
---------------
setState(() {});
}
}


class _VideoRecordingScreenState extends State<VideoRecordingScreen> {
//flag
bool _hasPermission = false;
-------------------
late final CameraController _cameraController;
// 카메라 초기화 메소드
Future<void> initCamera() async {
final cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
//전방 카메라
_cameraController = CameraController(
cameras[0],
ResolutionPreset.ultraHigh,
//후방 카메라
);
await _cameraController.initialize();
}
-------------------
return Scaffold(
backgroundColor: Colors.black,
-----------------
body: !_hasPermission
? null
: SizedBox(
width: MediaQuery.of(context).size.width,
child: const Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Initializing...",
style: TextStyle(
color: Colors.white,
fontSize: Sizes.size20,
),
),
Gaps.v20,
CircularProgressIndicator.adaptive(),
-----------------
],
),
),
);

import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:tiktok_clone/constants/gaps.dart';
import 'package:tiktok_clone/constants/sizes.dart';
class VideoRecordingScreen extends StatefulWidget {
const VideoRecordingScreen({super.key});
@override
State<VideoRecordingScreen> createState() => _VideoRecordingScreenState();
}
class _VideoRecordingScreenState extends State<VideoRecordingScreen> {
//flag
bool _hasPermission = false;
late final CameraController _cameraController;
// 카메라 초기화 메소드
Future<void> initCamera() async {
final cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
//전방 카메라
_cameraController = CameraController(
cameras[0],
ResolutionPreset.ultraHigh,
//후방 카메라
);
await _cameraController.initialize();
}
//권한 관련 메소드
Future<void> initPermissions() async {
final cameraPermission = await Permission.camera.request();
final micPermission = await Permission.microphone.request();
final cameraDenied =
cameraPermission.isDenied || cameraPermission.isPermanentlyDenied;
final micDenied =
micPermission.isDenied || micPermission.isPermanentlyDenied;
if (!cameraDenied && !micDenied) {
_hasPermission = true;
await initCamera();
setState(() {});
}
}
@override
void initState() {
super.initState();
initPermissions();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: SizedBox(
width: MediaQuery.of(context).size.width,
child: !_hasPermission || !_cameraController.value.isInitialized
? const Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
"Initializing...",
style:
TextStyle(color: Colors.white, fontSize: Sizes.size20),
),
Gaps.v20,
CircularProgressIndicator.adaptive()
],
)
: Stack(
alignment: Alignment.center,
children: [
CameraPreview(_cameraController),
],
),
),
);
}
}

IOS 실제폰에는 이거를 추가해야함

Check Settings -> Privacy -> Local Network.
이걸 on 해주기
참고 트러블 슈팅
1.info.plist

https://github.com/Jinwook-Song/tiktok_flutter/commit/c61a1cdae6ffed43f67106ec5f7850a9530546e0
permission handler의 경우
2.Podfile에도 permission 을 허용해주도록 수정해야합니다
3. 이거 문제는 아닌 듯해서 주석처리
Future initCamera()async {
...
setState(() {});
}
Future<void> initCamera() async {
final cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
_cameraController = CameraController(
// 0:back, 1: front
cameras[_isSelfieMode ? 1 : 0],
ResolutionPreset.ultraHigh,
);
await _cameraController.initialize();
_flashMode = _cameraController.value.flashMode;
--------------------
//iOS권한문제 수정
// setState(() {});
--------------------
}
//권한 관련 메소드
Future<void> initPermissions() async {
final cameraPermission = await Permission.camera.request();
final micPermission = await Permission.microphone.request();
final cameraDenied =
cameraPermission.isDenied || cameraPermission.isPermanentlyDenied;
final micDenied =
micPermission.isDenied || micPermission.isPermanentlyDenied;
if (!cameraDenied && !micDenied) {
_hasPermission = true;
await initCamera();
setState(() {});
}
}
@override
void initState() {
super.initState();
initPermissions();
--------------------
//iOS권한문제 수정
// setState(() {});
--------------------
}
Future<void> _toggleSelfieMode() async {
_isSelfieMode = !_isSelfieMode;
await initCamera();
setState(() {});
}
Future<void> _setFlashMode(FlashMode newFlashMode) async {
await _cameraController.setFlashMode(newFlashMode);
_flashMode = newFlashMode;
setState(() {});
}
@override
19.3 Selfie Mode
class _VideoRecordingScreenState extends State<VideoRecordingScreen> {
//flag
bool _hasPermission = false;
----------------
bool _isSelfieMode = false;
----------------
late CameraController _cameraController;
// 카메라 초기화 메소드
Future<void> initCamera() async {
final cameras = await availableCameras();
----------------
if (cameras.isEmpty) {
return;
}
_cameraController = CameraController(
// 0:back, 1: front
cameras[_isSelfieMode ? 1 : 0],
ResolutionPreset.ultraHigh,
);
await _cameraController.initialize();
}
----------------
initPermissions();
}
-----------------
Future<void> _toggleSelfieMode() async {
_isSelfieMode = !_isSelfieMode;
await initCamera();
setState(() {});
}
-----------------
@override
Widget build(BuildContext context) {
children: [
CameraPreview(_cameraController),
-------------------
Positioned(
top: Sizes.size20,
left: Sizes.size20,
child: IconButton(
color: Colors.white,
onPressed: _toggleSelfieMode,
icon: const Icon(
Icons.cameraswitch,
color: Colors.black,
-------------------
),
),
),
],
),
),
);

19.4 Flash Mode
bool _isSelfieMode = false;
---------------
late FlashMode _flashMode;
---------------
late CameraController _cameraController;
// 카메라 초기화 메소드
Future<void> initCamera() async {
ResolutionPreset.ultraHigh,
);
await _cameraController.initialize();
-----------------
_flashMode = _cameraController.value.flashMode;
-----------------
}
//권한 관련 메소드
Future<void> initPermissions() async {
setState(() {});
}
-----------------
Future<void> _setFlashMode(FlashMode newFlashMode) async {
await _cameraController.setFlashMode(newFlashMode);
_flashMode = newFlashMode;
setState(() {});
}
-----------------
@override
Widget build(BuildContext context) {
CameraPreview(_cameraController),
Positioned(
top: Sizes.size20,
-----------------
right: Sizes.size20,
child: Column(
children: [
IconButton(
color: Colors.black,
onPressed: _toggleSelfieMode,
icon: const Icon(
Icons.cameraswitch,
),
),
Gaps.v10,
IconButton(
color: _flashMode == FlashMode.off
? Colors.purple
: Colors.black,
onPressed: () => _setFlashMode(FlashMode.off),
icon: const Icon(
Icons.flash_off_rounded,
),
),
Gaps.v10,
IconButton(
color: _flashMode == FlashMode.always
? Colors.purple
: Colors.black,
onPressed: () => _setFlashMode(FlashMode.always),
icon: const Icon(
Icons.flash_on_rounded,
),
),
Gaps.v10,
IconButton(
color: _flashMode == FlashMode.auto
? Colors.purple
: Colors.black,
onPressed: () => _setFlashMode(FlashMode.auto),
icon: const Icon(
Icons.flash_auto_rounded,
),
),
Gaps.v10,
IconButton(
color: _flashMode == FlashMode.torch
? Colors.purple
: Colors.black,
onPressed: () => _setFlashMode(FlashMode.torch),
icon: const Icon(
Icons.flashlight_on_rounded,
-----------------
),
),
],
),
),
],
),
),
);
}
}

19.5 Recording Animation
positioned는 stack 안에서의 독립적인 공간
State<VideoRecordingScreen> createState() => _VideoRecordingScreenState();
}
class _VideoRecordingScreenState extends State<VideoRecordingScreen>
----------------
with SingleTickerProviderStateMixin {
----------------
//flag
bool _hasPermission = false;
----------------
//동영상 버튼용
late final AnimationController _animationController = AnimationController(
vsync: this,
duration: const Duration(
microseconds: 300,
),
);
late final Animation<double> _buttonAnimation =
Tween(begin: 1.0, end: 1.3).animate(_animationController);
----------------
_flashMode = newFlashMode;
setState(() {});
}
----------------
//손가락 한 번 누르면 동영상 녹화
void _onTapDown(TapDownDetails _) {
_animationController.forward();
}
//손가락 한 번 더 누르면 녹화 중지
void _onTapUp(TapUpDetails _) {
_animationController.reverse();
}
----------------
@override
Widget build(BuildContext context) {
late FlashMode _flashMode;
icon: const Icon(
Icons.flashlight_on_rounded,
),
),
],
),
),
-------------------
Positioned(
bottom: Sizes.size40,
child: GestureDetector(
onTapDown: _onTapDown,
onTapUp: _onTapUp,
child: ScaleTransition(
scale: _buttonAnimation,
child: Container(
width: Sizes.size80,
height: Sizes.size80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red.shade400,
-------------------
),
),
),
),

class _VideoRecordingScreenState extends State<VideoRecordingScreen>
-----------------
//두개 이상 애니일 때는 이거
with
TickerProviderStateMixin {
-----------------
//flag
bool _hasPermission = false;
----------------------
//동영상 버튼용
late final AnimationController _buttonAnimationController =
AnimationController(
vsync: this,
duration: const Duration(
microseconds: 300,
),
);
late final Animation<double> _buttonAnimation =
Tween(begin: 1.0, end: 1.3).animate(_buttonAnimationController);
late final AnimationController _progressAnimationController =
AnimationController(
vsync: this,
duration: const Duration(
seconds: 10,
),
lowerBound: 0.0,
upperBound: 1.0);
----------------------
late FlashMode _flashMode;
await initCamera();
setState(() {});
}
}
------------------------
@override
void initState() {
super.initState();
initPermissions();
_progressAnimationController.addListener(() {
setState(() {});
});
//addStatusListener는 애니메이션이 끝난 걸 알려주는 이벤트 리스너
_progressAnimationController.addStatusListener((status) {
if (status == AnimationStatus.completed) {
_stopRecording();
}
});
------------------------
//iOS권한문제 수정
// setState(() {});
}
_flashMode = newFlashMode;
setState(() {});
}
------------------------
//손가락 한 번 누르면 동영상 녹화(계속 누르고 있어야함)
void _startRecording(TapDownDetails _) {
_buttonAnimationController.forward();
_progressAnimationController.forward();
}
//손을 때던 10초 지나면 녹화 중지
void _stopRecording() {
_buttonAnimationController.reverse();
_progressAnimationController.reset();
}
------------------------
@override
Widget build(BuildContext context) {
Icons.flashlight_on_rounded,
),
),
],
),
),
Positioned(
bottom: Sizes.size40,
child: GestureDetector(
onTapDown: _startRecording,
----------------------
onTapUp: (details) => _stopRecording(),
----------------------
child: ScaleTransition(
scale: _buttonAnimation,
child: Stack(
alignment: Alignment.center,
children: [
----------------------
SizedBox(
width: Sizes.size80 + Sizes.size14,
height: Sizes.size80 + Sizes.size14,
child: CircularProgressIndicator(
color: Colors.red.shade400,
strokeWidth: Sizes.size6,
//value로 동영상 진행상태 표시
value: _progressAnimationController.value,
),
),
----------------------
Container(
width: Sizes.size80,
height: Sizes.size80,

19.6 startVideoRecording
Future<void> initCamera() async {
final cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
_cameraController = CameraController(
// 0:back, 1: front
cameras[_isSelfieMode ? 1 : 0],
ResolutionPreset.ultraHigh,
----------------
//Android 에뮬레이터 동영상 저장 플레이 안될때 버그 픽스
enableAudio: false,
);
----------------
await _cameraController.initialize();
----------------
//iOS만을 위한 설정
await _cameraController.prepareForVideoRecording();
----------------
_flashMode = _cameraController.value.flashMode;
//iOS권한문제 수정
// setState(() {});
}
//손을 때던 10초 지나면 녹화 중지
Future<void> _stopRecording() async {
if (!_cameraController.value.isRecordingVideo) return;
_buttonAnimationController.reverse();
_progressAnimationController.reset();
final video = await _cameraController.stopVideoRecording();
------------------
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VideoPreviewScreen(
video: video,
),
),
);
}
------------------
------------------
@override
void dispose() {
_progressAnimationController.dispose();
_buttonAnimationController.dispose();
_cameraController.dispose();
super.dispose();
}
------------------
@override
Widget build(BuildContext context) {
return Scaffold(

import 'dart:io';
import 'package:camera/camera.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoPreviewScreen extends StatefulWidget {
final XFile video;
const VideoPreviewScreen({
super.key,
required this.video,
});
@override
State<VideoPreviewScreen> createState() => _VideoPreviewScreenState();
}
class _VideoPreviewScreenState extends State<VideoPreviewScreen> {
late final VideoPlayerController _videoPlayerController;
Future<void> _initVideo() async {
_videoPlayerController = VideoPlayerController.file(
File(widget.video.path),
);
await _videoPlayerController.initialize();
await _videoPlayerController.setLooping(true);
await _videoPlayerController.play();
}
@override
void initState() {
super.initState();
_initVideo();
}
//dispose 메소드는 항상 구현해주자
@override
void dispose() {
_videoPlayerController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text("preview video"),
),
body: _videoPlayerController.value.isInitialized
? VideoPlayer(_videoPlayerController)
: null,
);
}
}

19.7 GallerySaver


여기에다가 permission handler 쓰지 않고 안드로이드 관련 마이크, 카메라도 허가 가능
class _VideoPreviewScreenState extends State<VideoPreviewScreen> {
late final VideoPlayerController _videoPlayerController;
------------------
bool _savedVideo = false;
------------------
Future<void> _initVideo() async {
_videoPlayerController = VideoPlayerController.file(
super.dispose();
}
---------------
Future<void> _saveToGallery() async {
if (_savedVideo) return;
await GallerySaver.saveVideo(widget.video.path, albumName: "TikTok Clone");
_savedVideo = true;
setState(() {});
}
---------------
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text("preview video"),
---------------
actions: [
IconButton(
onPressed: _saveToGallery,
icon: FaIcon(_savedVideo
? FontAwesomeIcons.check
: FontAwesomeIcons.download),
),
],
),
body: _videoPlayerController.value.isInitialized
? VideoPlayer(_videoPlayerController)
: null,
---------------
);
}
}
@override
Widget build(BuildContext context) {

19.8 ImagePicker


class VideoPreviewScreen extends StatefulWidget {
final XFile video;
-------------
//동영상 저장 아이콘은 사진찍고 확인할 스크린에서만 표시
final bool isPicked;
---------------
const VideoPreviewScreen({
super.key,
required this.video,
required this.isPicked,
title: const Text("preview video"),
actions: [
------------------
//동영상 저장 아이콘은 사진찍고 확인할 스크린에서만 표시
if (!widget.isPicked)
------------------
IconButton(
onPressed: _saveToGallery,
icon: FaIcon(
_savedVideo

final video = await _cameraController.stopVideoRecording();
----------------
if (!mounted) return;
----------------
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VideoPreviewScreen(
video: video,
----------------
//동영상 저장 아이콘은 사진찍고 확인할 스크린에서만 표시
isPicked: false,
----------------
),
),
);
}
-------------------
//갤러리 아이콘에서 비디오 보는 메소드
Future<void> _onPickVideoPressed() async {
final video = await ImagePicker().pickVideo(
source: ImageSource.gallery,
);
if (video == null) return;
if (!mounted) return;
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => VideoPreviewScreen(
video: video,
//동영상 저장 아이콘은 사진찍고 확인할 스크린에서만 표시
isPicked: true,
),
),
);
}
-------------------
icon: const Icon(
Icons.flashlight_on_rounded,
),
),
],
),
),
-------------------
Positioned(
bottom: Sizes.size40,
width: MediaQuery.of(context).size.width,
child: Row(
children: [
//빈 공간처럼 표시되어 레코딩 버튼을 중간으로 이동
const Spacer(),
GestureDetector(
-------------------
onTapDown: _startRecording,
onTapUp: (details) => _stopRecording(),
child: ScaleTransition(
scale: _buttonAnimation,
-------------------
child: Stack(
alignment: Alignment.center,
-------------------
children: [
SizedBox(
width: Sizes.size80 + Sizes.size14,
height: Sizes.size80 + Sizes.size14,
child: CircularProgressIndicator(
color: Colors.red.shade400,
strokeWidth: Sizes.size6,
//value로 동영상 진행상태 표시
value: _progressAnimationController.value,
),
),
Container(
width: Sizes.size80,
height: Sizes.size80,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red.shade400,
),
),
],
),
),
),
-------------------
Expanded(
child: Container(
alignment: Alignment.center,
child: IconButton(
onPressed: _onPickVideoPressed,
icon: const FaIcon(
FontAwesomeIcons.image,
color: Colors.black,
-------------------
)),
),
)
],
),
)
],
),
),
);
}
}
@override
Widget build(BuildContext context) {
return Scaffold(

19.9 AppLifecycleState
지금 버전의 카메라 플러그인의 lifecycle은 수동으로 바꿔줘야한다. state를 우리가 따로 관리 해줘야한다.
@override
State<VideoRecordingScreen> createState() => _VideoRecordingScreenState();
}
class _VideoRecordingScreenState extends State<VideoRecordingScreen>
//두개 이상 애니일 때는 이거
with
TickerProviderStateMixin,
---------------------
// camera가 백그라운드에 있을때는 dispose하는 것
WidgetsBindingObserver {
---------------------
//flag
bool _hasPermission = false;
bool _isSelfieMode = false;
@override
void initState() {
super.initState();
initPermissions();
---------------------
// camera가 백그라운드에 있을때는 dispose하는 것
WidgetsBinding.instance.addObserver(this);
---------------------
_progressAnimationController.addListener(() {
setState(() {});
});
_cameraController.dispose();
super.dispose();
}
@override
---------------------
// camera가 백그라운드에 있을때는 dispose하는 것
Future<void> didChangeAppLifecycleState(AppLifecycleState state) async {
//권한 관련 버그 픽스
if (state == AppLifecycleState.inactive) {
// 오직 카메라가 initialize디어을 때만 dispose 호출하기
if (!_cameraController.value.isInitialized) return;
_cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
await initCamera();
setState(() {});
}
}
---------------------
//갤러리 아이콘에서 비디오 보는 메소드
Future<void> _onPickVideoPressed() async {
//어플을 새로깔 때 권한 다시 묻는데 지금 코드 순서는 권한을 가져온 후 카메라를 initialize하기 때문에 플러터는 어픟이 비활성화 되었다고 생각한다.
즉, 앱은 유저가 떠났을 때만 비활성화되는게 아니다. 권한 요청창이 앺 앞에서 나타날 때도 고려해야함

그래서 이런식으로 고쳐 줌
1. 우리가 권한을 가지고 있지 않은 것을(즉, camera controller가 initialize 되지 않은 것) 확인하기
// 카메라 초기화 메소드
Future<void> initCamera() async {
final cameras = await availableCameras();
if (cameras.isEmpty) {
return;
}
_cameraController = CameraController(
// 0:back, 1: front
cameras[_isSelfieMode ? 1 : 0],
ResolutionPreset.ultraHigh,
//Android 에뮬레이터 동영상 저장 플레이 안될때 버그 픽스
enableAudio: false,
);
await _cameraController.initialize();
//iOS만을 위한 설정
await _cameraController.prepareForVideoRecording();
_flashMode = _cameraController.value.flashMode;
//iOS권한문제 수정
// setState(() {});
--------------
setState(() {});
--------------
}
//권한 관련 메소드
Future<void> initPermissions() async {
@override
// camera가 백그라운드에 있을때는 dispose하는 것
void didChangeAppLifecycleState(AppLifecycleState state) {
----------------
//권한 관련 버그 픽스1
if (!_hasPermission) return;
//권한 관련 버그 픽스2
// 오직 카메라가 initialize되어을 때만 dispose 호출하기
if (!_cameraController.value.isInitialized) return;
----------------
if (state == AppLifecycleState.inactive) {
_cameraController.dispose();
} else if (state == AppLifecycleState.resumed) {
initCamera();
}
}
//갤러리 아이콘에서 비디오 보는 메소드
Future<void> _onPickVideoPressed() async {
didChangeAppLifecycleState future없애고 initCamera에 setState옮긴건 관계는 없는데
옮긴다면 이런식으로 옮길것
직접 카메라 UI 구현하고 싶다면 camera package보다 camerAwesome package를 더 추천한다. 더 feature가 많다.