[앱개발]/Flutter, Dart, Figma

[플러터] 비디오 플레이어 앱 만들기

기록자_Recordian 2025. 5. 25. 20:26
728x90
반응형
이전 내용
 

[플러터] 주사위 앱 만들기

이전 내용 [플러터] 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

728x90
반응형