플러터 (flutter)

Flutter에서 비디오 재생하기 -Video player

CreatoMaestro 2023. 7. 1. 14:25
반응형

이번 글에서는 저번 글에서 살펴 본 ImagePicker를 이용해 비디오를 불러오고 재생하는 앱을 만들어본다.

ImagePicker에 대해선 저번 글을 살펴본다.

2023.06.30 - [단기 프로젝트] - 갤러리에서 이미지를 선택하는 flutter ImagePicker (프로젝트 1_2 )

 

갤러리에서 이미지를 선택하는 flutter ImagePicker (프로젝트 1_2 )

이번에는 갤러리에서 이미지를 선택하는 ImagePicker에 대해 알아본다. 기능은 다음과 같다. 1) 홈 화면에서 floating action button을 클릭하면 선택할 수 있는 갤러리로 넘어간다. 2) 갤러리에서 사진을

ti-project-11.tistory.com

 

우선 pubspce.yaml에서 video_player를 추가해준다.

dependencies:
  flutter:
    sdk: flutter


  # The following adds the Cupertino Icons font to your application.
  # Use with the CupertinoIcons class for iOS style icons.
  cupertino_icons: ^1.0.2
  image_picker: 1.0.0
  video_player: 2.7.0

그 다음 android/app/src 폴더 안에 있는 build.gradle로 들어간다.

아 파일에 있는 defaultConfig에서 minSdkVersion을 21로 바꿔준다.

이렇게 해주는 이유는 ImagePicker의 최소 SDK 버전이 21이기 때문이다.

defaultConfig {
    // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
    applicationId "com.example.flutter_example"
    // You can update the following values to match your application needs.
    // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration.
    minSdkVersion 21
    targetSdkVersion flutter.targetSdkVersion
    versionCode flutterVersionCode.toInteger()
    versionName flutterVersionName
}

이후 manifest.xml 파일에 READ_EXTERNAL_STORAGE 퍼미션을 넣어준다.

이것을 통해 앱이 갤러리에 접근할 수 있도록 한다.

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
    <application
        android:label="flutter_example"
        android:name="${applicationName}"
        android:icon="@mipmap/ic_launcher">

 

이제 main.dart에 코드를 작성한다.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_player/video_player.dart';

void main() {
  runApp(VideoWallPaper());
}

 

필요한 라이브러리를 import해주고, main 함수 안에 VideoWallPaper 클래스의 생성자를 runApp 함수의 파라미터로 넣어준다.

각각의 import는 다음과 같은 역할을 한다.

  • dart:io : 파일 입출력을 위해 필요하다
  • material.dart : material 디자인의 스타일을 이용 가능하도록 해준다.
  • image_picker.dart : 갤러리에서 이미지를 선택할 수 있도록 한다.
  • video_player.dart : 비디오 재생을 위해 필요하다.
class VideoWallPaper extends StatefulWidget {

  //Creater
  VideoWallPaper({Key? key}) : super(key: key);

  @override
  State<VideoWallPaper> createState() => _VideoWallPaperState();
}

VideoWallPaper 클래스를 StatefulWidget으로 생성해준다.

생성자를 생성해주고, State도 생성해준다.

State 클래스는 _VideoWallPaperState이다.

 

class _VideoWallPaperState extends State<VideoWallPaper> {

  XFile? video;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.initState();
  }

  void floatingPressed() async {
    final video = await ImagePicker().pickVideo(source: ImageSource.gallery);
    setState(() {
      this.video = video;
    });
  }

VideoWallPaper의 상태 클래스인 _VideoWallPaperState를 생성해준다.

XFile? video는 갤러리에서 받아올 비디오를 담을 변수이다.

 

initState()는 위젯이 생성될 때 실행되고, dispose는 위젯의 생명 주기가 끝날  때 실행된다.

FloatingPressed 함수

floatingPressed는 floatingActionButton이 클릭되었을 때 실행될 함수이다.

함수에서는 ImagePicker를 이용해 갤러리에서 비디오를 받아온다.

pickVideo는 비디오를 받아오는 메서드이고, ImageSource.gallery는 비디오를 갤러리에서 받아옴을 의미한다.

비디오를 받아올 때 까지 기다려야 하기 때문에 await을 이용해 받아올 때 까지 기다리도록 만든다.

이후 setState를 통해 받아온 video를 클래스의 video 변수에 넣어준다.

 

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: Scaffold(
        floatingActionButton: FloatingActionButton(
          onPressed: floatingPressed,
          child: Icon(Icons.change_circle),
        ),
        body: video != null? VideoWidget(video: video!) : initScreen(),
      ),
    );
  }
}

이후 build 함수를 생성해준다.

theme는 dark 테마로 해준다.

Scaffold함수에는 floatingActionButton과  body함수가 있다.

floatingActionButton은 클릭했을 때 floatingPressed가 실행되도록 하고, 아이콘을 버튼 안에 넣어준다.

 

body는 video == null일 경우 initScreen을 null이 아닐 경우 VideoWidget을 화면에 띄우도록 한다.

 

Widget initScreen() {
  return const Center(
    child: Text(
      "Please select video!",
      style: TextStyle(
        fontSize: 20.0,
      ),
    ),
  );
}

이 코드는 initScreen 함수이다. 

위젯을 반환해준다.

중앙정렬된 Text 위젯을 반환해준다.

 

class VideoWidget extends StatefulWidget {
  XFile video;
  VideoWidget({required this.video, Key? key}):super(key: key);

  @override
  State<VideoWidget> createState() => _VideoWidgetState();
}

 

다음 클래스는 VideoWidget클래스이다.

이 클래스에서는 video player를 구축한다.

XFile은 비디오를 담을 변수의 타입이다.

_VideoWallPaperState에서 video를 받아 저장한다.

 

class _VideoWidgetState extends State<VideoWidget>{
  VideoPlayerController? videoController;


  void initVideo() async{
    final videoController = VideoPlayerController.file(File(widget.video.path));
    await videoController.initialize();

    videoController!.addListener(videoListener);

    setState(() {
      this.videoController = videoController;
    });
  }

  void videoListener() {
    setState(() {

    });
  }

 

이제 VideoWidget의 상태 클래스인 _VideoWidgetState 클래스를 생성해준다.

이 안에 VideoPlayController 타입의 변수인 videoController를 생성해준다.

 

안에 2개의 메서드가 있는데 각각은 다음과 같은 역할을 한다.

  • initVideo : video파일을 받아 이 파일을 이용해 videoController를 초기화해준다.
  • videoListener : videoController의 상태가 바뀌면 setState() 메서드를 실행해 상태를 바꿔준다.

initState에서는 videoController를 초기화해준다.

이후 상태가 바뀔 때마다 위젯을 바꿔주기 위해 리스너를 등록해준다. 

리스너는 videoListener로 해준다.

 

이후 setState를 통해 this.videoController 안에 videoController를 넣어준다.

@override
void initState() {
  super.initState();
  initVideo();
}

@override
void dispose() {
  super.dispose();
  videoController!.removeListener(videoListener);
}

 

방금 만든 initVideo 메서드를 initState 메서드 안에 넣어준다.

이렇게 하면 위젯이 생성될 때 자동으로 initVideo를 실행하게 된다.

 

@override
void didUpdateWidget(covariant VideoWidget oldWidget) {
  super.didUpdateWidget(oldWidget);
  videoController = null;
  initVideo();
}

다음 코드는 동영상을 다시 선택할 때 실행되는 함수이다.

VideoWidget이 업데이트되면 실행된다.

videoController를 null로 만들어주는 것은 Please Wait을 띄우기 위함이다.

이후 initVideo를 다시 실행하여 videoController를 바꿔준다.

 

위젯이 업데이트 될 때 마다 리스너를 삭제해주어야 하기 때문에 dispose 메서드에서 등록된 리스너를 삭제해준다.

  Widget backButton() {
    return IconButton(
        onPressed: (){
          videoController!.seekTo(videoController!.value.position - Duration(seconds: 3));
        },
        icon: const Icon(Icons.arrow_back_rounded),
    );
  }

  Widget frontButton() {
    return IconButton(
      onPressed: (){
        videoController!.seekTo(videoController!.value.position + Duration(seconds: 3));
      },
      icon: const Icon(Icons.arrow_forward_rounded),
    );
  }

  Widget playOrPressed() {
    Widget icon;
    if(videoController!.value.isPlaying) {
      icon = Icon(Icons.play_arrow);
    } else {
      icon = Icon(Icons.pause);
    }
    return IconButton(
      onPressed: (){
        if(videoController!.value.isPlaying) {
          videoController!.pause();
        } else {
          videoController!.play();
        }
      },
      icon: icon,
    );
  }
}

 

다음은 videoController를 컨트롤 하기 위한 버튼을 정의해준다.

뒤로 가기, 앞으로 가기, 재생/정지 3개의 버튼을 만들어준다.

 

Widget backButton() {
  return IconButton(
      onPressed: (){
        videoController!.seekTo(videoController!.value.position - Duration(seconds: 3));
      },
      icon: const Icon(Icons.arrow_back_rounded),
  );
}

 

backButton은 뒤로 가기 버튼이다.

버튼이 눌리면 3초 뒤로 돌아간다.

seekTo 메서드를 통해 동영상를 컨트롤한다.

videoControll!.value.position은 현재 동영상이 재생하고 있는 위치를 Duration 타입으로 나타낸다.

Duration(seconds:3)을 빼주면 3초 뒤로 가게 된다.

 

Widget frontButton() {
  return IconButton(
    onPressed: (){
      videoController!.seekTo(videoController!.value.position + Duration(seconds: 3));
    },
    icon: const Icon(Icons.arrow_forward_rounded),
  );
}

 

앞으로 가기 버튼도 같은 방식으로 생성해준다.

Widget playOrPressed() {
  Widget icon;
  if(videoController!.value.isPlaying) {
    icon = Icon(Icons.pause);
  } else {
    icon = Icon(Icons.play_arrow);
  }
  return IconButton(
    onPressed: (){
      if(videoController!.value.isPlaying) {
        videoController!.pause();
      } else {
        videoController!.play();
      }
    },
    icon: icon,
  );
}

 

위 코드는 재생/일시 정지 버튼이다.

IconButton을 반환하는데 이 때 Icon의 모양은 동영상 플레이어 상태에 따라 변경된다.

 

만약 플레이어가 재생중이면 (value.isPlaying == true) 버튼 모양을 pause로 바꾸고, 반대의 경우 play_arrow로 바꾼다.

 

onPressed를 통해 버튼이 눌렸을 때 실행할 코드를 정의한다.

만약 재생 상태면 동영상을 멈추고(pause()메서드), 멈춰있는 상태이면 재생한다.(play()메서드)

Widget build(BuildContext context) {
  if(videoController == null) {
    return Center(
      child: Text(
        "Please Wait...",
        style: TextStyle(
          fontSize: 30.0,
        ),
      ),
    );
  }

  return GestureDetector(
    onTap: () {setState(() {
      showController = !showController;
    });},
    child: AspectRatio(aspectRatio: videoController!.value.aspectRatio,
      child: Stack(
        children: [
          VideoPlayer(
          videoController!,
        ),
          if(showController)
          Positioned(
            bottom: 0,
              left: 0,
              right: 0,
              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(),
              ),
          ),
          if(showController)
          Align(
            alignment: Alignment.center,
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceEvenly,
              children: [
                backButton(),
                playOrPressed(),
                frontButton(),
              ],
            ),
          ),
      ]
      )
    ),
  );
}

 

이제 build 함수를 정의해준다.

만약 videoController에 값이 들어오지 않은 상태라면, (videoController == null) Please Wait이라는 글자를 띄운다.

이후 값이 들어오면 비디오 플레이어를 띄운다.

 

가장 상위 위젯으로 GestureDetector가 있는데, 이는 화면을 탭 했는지 확인하기 위함이다.

화면을 탭 할때마다 컨트롤 버튼이 나타나거나 사라진다.

 

child로는 AspectRatio 위젯이 들어있다. AspectRatio는 화면 비율에 맞춰 위젯을 맞춰준다.

동영상 전체를 화면에 맞춰 보기 위해 사용한다.

 

AspectRatio의 child로는 Stack 위젯을 넣었다.

Stack을 통해 비디오 위해 컨트롤 버튼을 띄운다.

 

Stack 안에는 Positioned와 Align 위젯이 위치해있다.

Positioned는 Slider가 동영상 밑에 위치하도록 하고, Align은 버튼이 가운데에 위치하도록 한다.

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(),
),

위 코드는 Slider 위젯 코드이다.

onChanged는 슬라이더를 터치하여 값을 바꾸었을 때 어떤 코드를 실행할지 정해준다.

여기서는 슬라이더에 맞춰 video의 시간을 맞춰준다. (seekTo 메서드 이용)

value는 현재 값을 의미하고, min, max는 최소 최대 값을 의미한다.

max 값으로는 비디오의 최대 시간을 넣어준다.

 

Align(
  alignment: Alignment.center,
  child: Row(
    mainAxisAlignment: MainAxisAlignment.spaceEvenly,
    children: [
      backButton(),
      playOrPressed(),
      frontButton(),
    ],
  ),
),

다음 코드는 Align 코드이다. 

이 코드는 Row 위젯이 센터에 위치하도록 만들어준다.

 

Row 안에는 위에서 만들어준 버튼들을 넣어준다.

 

앱을 다 만들면 다음과 같이 나온다.

앱 실행 결과

처음에는 맨 왼쪽 화면이 나타난다.

여기서 오른쪽 아래에 있는 버튼을 누르면 갤러리로 들어간다.

클릭하면 로딩되는 동안 Please Wait이 뜬다.

로딩이 끝나면 동영상이 뜨게 된다.

 

다음은 전체 코드이다.

import 'dart:io';
import 'package:flutter/material.dart';
import 'package:image_picker/image_picker.dart';
import 'package:video_player/video_player.dart';

void main() {
  runApp(VideoWallPaper());
}

class VideoWallPaper extends StatefulWidget {

  //Creater
  VideoWallPaper({Key? key}) : super(key: key);

  @override
  State<VideoWallPaper> createState() => _VideoWallPaperState();
}

class _VideoWallPaperState extends State<VideoWallPaper> {

  XFile? video;

  @override
  void initState() {
    super.initState();
  }

  @override
  void dispose() {
    super.initState();
  }

  void floatingPressed() async {
    final video = await ImagePicker().pickVideo(source: ImageSource.gallery);
    setState(() {
      this.video = video;
    });
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      theme: ThemeData.dark(),
      home: Scaffold(
        floatingActionButton: FloatingActionButton(
          onPressed: floatingPressed,
          child: Icon(Icons.change_circle),
        ),
        body: video != null? VideoWidget(video: video!) : initScreen(),
      ),
    );
  }
}

class VideoWidget extends StatefulWidget {
  XFile video;
  VideoWidget({required this.video, Key? key}):super(key: key);

  @override
  State<VideoWidget> createState() => _VideoWidgetState();
}

class _VideoWidgetState extends State<VideoWidget>{
  VideoPlayerController? videoController;

  bool showController = true;


  void initVideo() async{
    final videoController = VideoPlayerController.file(File(widget.video.path));
    await videoController.initialize();

    videoController!.addListener(videoListener);

    setState(() {
      this.videoController = videoController;
    });
  }

  void videoListener() {
    setState(() {

    });
  }

  @override
  void didUpdateWidget(covariant VideoWidget oldWidget) {
    super.didUpdateWidget(oldWidget);
    videoController = null;
    initVideo();
  }

  @override
  void initState() {
    super.initState();
    initVideo();
  }

  @override
  void dispose() {
    super.dispose();
    videoController!.removeListener(videoListener);
  }

  Widget build(BuildContext context) {
    if(videoController == null) {
      return Center(
        child: Text(
          "Please Wait...",
          style: TextStyle(
            fontSize: 30.0,
          ),
        ),
      );
    }

    return GestureDetector(
      onTap: () {setState(() {
        showController = !showController;
      });},
      child: AspectRatio(aspectRatio: videoController!.value.aspectRatio,
        child: Stack(
          children: [
            VideoPlayer(
            videoController!,
          ),
            if(showController)
            Positioned(
              bottom: 0,
                left: 0,
                right: 0,
                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(),
                ),
            ),
            if(showController)
            Align(
              alignment: Alignment.center,
              child: Row(
                mainAxisAlignment: MainAxisAlignment.spaceEvenly,
                children: [
                  backButton(),
                  playOrPressed(),
                  frontButton(),
                ],
              ),
            ),
        ]
        )
      ),
    );
  }

  Widget backButton() {
    return IconButton(
        onPressed: (){
          videoController!.seekTo(videoController!.value.position - Duration(seconds: 3));
        },
        icon: const Icon(Icons.arrow_back_rounded),
    );
  }

  Widget frontButton() {
    return IconButton(
      onPressed: (){
        videoController!.seekTo(videoController!.value.position + Duration(seconds: 3));
      },
      icon: const Icon(Icons.arrow_forward_rounded),
    );
  }

  Widget playOrPressed() {
    Widget icon;
    if(videoController!.value.isPlaying) {
      icon = Icon(Icons.pause);
    } else {
      icon = Icon(Icons.play_arrow);
    }
    return IconButton(
      onPressed: (){
        if(videoController!.value.isPlaying) {
          videoController!.pause();
        } else {
          videoController!.play();
        }
      },
      icon: icon,
    );
  }
}

Widget initScreen() {
  return const Center(
    child: Text(
      "Please select video!",
      style: TextStyle(
        fontSize: 20.0,
      ),
    ),
  );
}

 

*"글에서 사용한 영 저작물은 한양도성도에서 2016년 작성하여 공공누리 제1유형으로 개방한 '숭례'을 이용하였으며,해당 저작물은 'https://www.kogl.or.kr/recommend/recommendDivView.doatcUrl=personal&recommendIdx=3737&division=video'

에서 무료로 다운받으실 수 있습니다."

반응형