[플러터] 비디오 플레이어 앱 만들기
이전 내용
[플러터] 주사위 앱 만들기
이전 내용 [플러터] D-Day 앱 만들기 (feat. 폰트 반영하기, Cupertino 위젯)이전 내용 [플러터] 이미지 롤링 기능 구현하기이전 내용 [플러터] 웹 앱 만들어보기에러 관련 [플러터] 에러 발생 및 해결1. G
puppy-foot-it.tistory.com
비디오 플레이어 앱 만들기
핸드폰 갤러리에 있는 동영상을 실행할 수 있는 비디오 플레이어 앱을 구현해 본다.
※ 프로젝트 (파일) 이름을 video_player 라고 짓게 되면 플러그인 이름과 겹치게 되므로, 다른 이름으로 지어야 한다.
◆ 동영상 넣기
먼저 assets 디렉터리를 생성한 뒤, video 라는 디렉터리를 생성하여 애뮬레이터로 실행할 동영상들을 넣어준다.
그리고나서 영상 파일을 모두 선택한 후 애뮬레이터로 드래그 & 드롭하고나서
제대로 잘 들어갔는지 확인하기 위해
[Files] - Downloads 에 들어가서 확인해 본다.
또한, 재생 버튼 이미지도 assets 디렉터리에 img 폴더를 만들고 그 안에 넣어준다.
그리고 pubspec.yaml에 에셋을 추가해 준다.
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
image_picker: 1.1.2 # 추가
video_player: 2.9.2 # 추가
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
assets: # 추가
- assets/img/
그런 다음 변경 사항을 반영해 준다. (pub get)
◆ 권한 추가하기
이전에 이미지 롤링 기능을 구현할 때처럼 권한을 추가해 줘야 한다.
AndroidManifest.xml 파일에 아래 내용을 입력한다.
android/app/src/main/res/AndroidManifest.xml
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
주요 파일 구조
코드 및 코드 설명
◆ main.dart
import 'package:flutter/material.dart';
import 'package:videos_player/screen/home_screen.dart'
void main() {
runApp(
MaterialApp(
home: HomeScreen(),
),
);
}
◆ home_screen.dart
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:videos_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: [
_Play( // 로고 이미지(플레이 버튼)
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(0xFF0001118),
],
),
);
}
Widget renderVideo() { // 동영상 선택 후 보여줄 위젯
return Center(
child: CustomVideoPlayer( // 동영상 재생 플레이어 위젯
video: video!, // 선택된 동영상 입력
onNewVideoPressed: onNewVideoPressed,
),
);
}
}
class _Play extends StatelessWidget { // 로고 보여줄 위젯
final GestureTapCallback onTap; // 탭했을 때 실행 함수
const _Play({
required this.onTap,
Key? key,
}) : super(key: key);
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: onTap, // 상위 위젯으로부터 탭 콜백
child: Image.asset(
'assets/img/play_button.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: TextSytle,
),
Text(
'PLAYER',
style:textStyle.copyWith(
fontWeight: FontWeight.w700,
),
),
],
);
}
}
◆ 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:videos_player/component/custom_icon_button.dart';
// 동영상 위젯 생성
class CustomVideoPlayer extends StatefulWidget {
// 선택한 동영상 저장 변수
// XFile: ImagePicker로 영상 또는 이미지 선택시 반환 타입
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> {
bool showControls = false; // 동영상 조작 아이콘 보일지 선택
// 동영상 조작 컨트롤러
VideoPlayerController? videoController;
@override
// convariant: CustomVideoPlayer 클래스의 상속값 허용
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 = VideoController.file(
File(widget.video.path),
);
await videoController.initialize();
// 컨트롤러 속성이 변경될 때마다 실행될 함수
videoController.addListener(videoControllerListener);
setState(() {
this.videoController = videoController;
});
}
// 동영상 재생 상태 변경 시마다 setState() 실행하여 build() 재실행
void videoControllerListener() {
setState(() {});
}
// State가 폐기될 때 같이 폐기할 함수 실행
@override
void dispose() {
// Listener 삭제
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 위젯을 위로 쌓을 수 있는 위젯
children: [
VideoPlayer( // VideoPlayer 위젯을 Stack으로 이동
videoController!,
),
if(showControls)
Container( // 아이콘 버튼 보일 때 화면 어둡게 변경
color: Colors.black.withOpacity(0.5),
),
Positioned( // child 위젯의 위치를 정할 수 있는 위젯
bottom: 0,
right: 0,
left: 0,
child: Padding(
padding: EdgeInsets.symmetric(horizontal: 8.0),
child: Row(
children: [
renderTimeTextFromDuration(
// 동영상 현재 위치
videoController!.value.position,
),
Expanded(
// Slider가 남는 공간을 모두 차지하도록 구현
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,
),
],
),
),
),
// showControls가 true일 때만 아이콘 보여주기
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.roate_right,
),
],
),
],
),
)
);
}
Widget renderTimeTextFromDuration(Duration duration) {
// Duration 값을 보기 편한 형태로 변환
return Text(
'${duration.inMunutes.toString().padLeft(2, '0')}: ${(duration.inSeconds %60).toString().padLeft(2, '0')}',
style: TextStyle(
color: Colors.white,
),
);
}
void onReversPressed() { // 되감기 버튼 눌렀을 때 실행될 함수
final currentPosition = videoController!.value.position; // 현재 실행 중인 위치
Duration position = Duration(); // 0초로 실행 위치 초기화
if (currentPosition.inSeconds > 3) { // 현재 실행 위치가 3초 보다 길 때만 3초 빼기
position = currentPosition - Duration(seconds: 3);
}
videoController!.seekTo(position);
}
void onForwardPressed() { // 앞으로 감기 버튼 눌렀을 때 실행될 함수
final maxPosition = videoController!.value.duration; // 동영상 길이
final currentPosition = videoController!.value.position; // 현재 실행 중인 위치
Duration position = maxPosition; // 동영상 길이로 실행 위치 초기화
// 동영상 길이에서 3초를 뺀 값보다 현재 위치가 짧을 때만 3초 더하기
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();
}
}
}
◆ 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 IconButtom( // 아이콘을 버튼으로 만들어주는 위젯
onPressed: onPressed, // 아이콘 눌렀을 때 실행될 함수
iconSize: 30.0, // 아이콘 크기
color: Colors.white, // 아이콘 색상
icon: Icon( // 아이콘
iconData,
),
);
}
}
[참고]
코드팩토리의 플러터 프로그래밍
다음 내용
[플러터] 위치 기반 앱 만들기
이전 내용 [플러터] 비디오 플레이어 앱 만들기이전 내용 [플러터] 주사위 앱 만들기이전 내용 [플러터] D-Day 앱 만들기 (feat. 폰트 반영하기, Cupertino 위젯)이전 내용 [플러터] 이미지 롤링 기능 구
puppy-foot-it.tistory.com