⭐️ 개발/플러터

[프로젝트] 영상플레이어 (4) 동영상 위 버튼 올리기

짱구러버 2022. 12. 15. 18:55
728x90

들어가기 앞서...

동영상위의 버튼 플레이 버튼을 만들고 컨트롤 해보자!


본문으로...

1.  비디오 플레이어를 원래 사이즈로 만들까?

  @override
  Widget build(BuildContext context) {
    if (videoController == null) {
      return CircularProgressIndicator();
    }
    return AspectRatio(
        aspectRatio: videoController!.value.aspectRatio,
        child: Stack(
          children: [
            VideoPlayer(videoController!),
            _Controls(),
          ],
        ));
  }
}

class _Controls extends StatelessWidget {
  const _Controls({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        IconButton(
            onPressed: () {},
            iconSize: 30.0,
            color: Colors.white,
            icon: Icon(Icons.play_arrow))
      ],
    );
  }

1)  아이콘 하나를 만들었는데, 공통되는 함수로 만들어서 분리시키자!

20

 

class _Controls extends StatelessWidget {
  const _Controls({Key? key}) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.spaceEvenly,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        renderIconButton(onPressed: () {}, iconData: Icons.rotate_left),
        renderIconButton(onPressed: () {}, iconData: Icons.play_arrow),
        renderIconButton(onPressed: () {}, iconData: Icons.rotate_right),
      ],
    );
  }

  // 중복되는 아이콘 함수로 빼기
  Widget renderIconButton({
    required VoidCallback onPressed,
    required IconData iconData,
  }) {
    return IconButton(
        onPressed: onPressed,
        iconSize: 30.0,
        color: Colors.white,
        icon: Icon(iconData));
  }
}

2) 상단 갤러리 버튼 만들어주자!

...
child: Stack(
          children: [
            VideoPlayer(videoController!),
            _Controls(),
            Positioned(
              // 오른쪽에 0 픽셀만큼 위치시킨다.
              right: 0,
              child: IconButton(
                  onPressed: () {},
                  color: Colors.white,
                  iconSize: 30.0,
                  icon: Icon(
                    Icons.photo_camera_back,
                  )),
            )
          ],
        )
...

 

2. 플레이할때 분기처리!

재생중에는 일시정지 버튼으로, 일시정지중에는 재생버튼으로 분기처리해야한다.

그러기위해선, 현재 비디오가 플레이 중인지 아닌지를 알면된다.

// 클래스 부분 
_Controls(
  onPlayPressed: onPlayPressed,
  onReversePressed: onReversePressed,
  onForwardPressed: onForwardPressed,
  isPlaying: videoController!.value.isPlaying,
),

...
// 함수부분
void onPlayPressed() {
    setState(() {
      if (videoController!.value.isPlaying) {
        // 실행중이면 중지
        videoController!.pause();
      } else {
        // 실행중 아니면 실행
        videoController!.play();
      }
    });
  }
  
...
// 버튼 부분
renderIconButton(
  onPressed: onPlayPressed,
  iconData: isPlaying ? Icons.pause : Icons.play_arrow
),

3. 이전 버튼, 이후 버튼 함수 추가!

void onPlayPressed() {
    setState(() {
      if (videoController!.value.isPlaying) {
        // 실행중이면 중지
        videoController!.pause();
      } else {
        // 실행중 아니면 실행
        videoController!.play();
      }
    });
  }

  void onReversePressed() {
    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; // 전체 영상의 길이

    if ((maxPosition - Duration(seconds: 3)).inSeconds >currentPosition.inSeconds ) {
      // 전체 시간 - 3 초 > 현재 초
      position = currentPosition + Duration(seconds: 3);
    }
	
    // 현재 시간부터 재생을 다시 할 수 있게 position을 넘겨준다.
    videoController!.seekTo(position);

  }

4. 슬라이더 만들기

// 영상이 재생시간을 저장할 변수
Duration currentPosition = Duration();

...
children: [
...
 Positioned(
  bottom: 0,
  right: 0,
  left: 0,
  child: Padding(
  // 패딩 추가
    padding: const EdgeInsets.symmetric(horizontal: 8.0),
    child: Row(
      children: [
      // 1번 설명 (currentPosition.inSeconds % 60)
      // 2번 설명 (.padLeft(2,"0"))
        Text(
          '${currentPosition.inMinutes}:${(currentPosition.inSeconds % 60).toString().padLeft(2,"0")}',
          style: TextStyle(
            color: Colors.white,
          ),
        ),
        Expanded(
          child: Slider(
            value: currentPosition.inSeconds.toDouble(),
            onChanged: (double val) {
              setState(() {
                currentPosition = Duration(seconds: val.toInt());
              });
            },
            max: videoController!.value.duration.inSeconds.toDouble(),
            min: 0,
          ),
        ),
        Text(
          '${videoController!.value.duration.inMinutes}:${(videoController!.value.duration.inSeconds % 60).toString().padLeft(2,"0")}',
          style: TextStyle(
            color: Colors.white,
          ),
        ),
      ],
    ),
  ),
)
...

]

1번 설명) 

currentPostion.inSeconds 는 영상의 총길이를 초로 나타낸 값이다. 

그렇다면 영상이 1분이넘어가면 80초 이렇게 나올수 있는데, 앞에 inMinutes 를 사용해서 분을 표현했으니, 초에서는 60초 이상이 출력되면 안된다.

그래서 60 으로 나눠서 나머지값만 출력하면 이상없다.

2번 설명)

.padLeft(2,' 0') 는 최대 길이는 2자리이고, 나머지 자리를 채우는 것은 '0' 으로 설정한 것이다..

 

출처 

https://api.flutter.dev/flutter/dart-core/String/padLeft.html

 

5. 비디오와 슬라이더가 싱크가 맞게 해주기

현재까지는 슬라이더를 클릭하면 해당 초에서 재생이 되지 않는다.

그 이유는 Sider() 위젯의 onChange 함수가 슬라이더만 클릭해서 실행이되고, 재생을 눌렀을때는 다시 0 초 부터 시작을 하기 때문이다.

그래서 onChange 함수에 비디오컨트롤러의 현재 시간을 보내주고 플레이를 시켜주면 된다.

Slider(
    value: currentPosition.inSeconds.toDouble(),

    onChanged: (double val) {
       // 기존   
       // setState(() {
       //   currentPosition = Duration(seconds: val.toInt());
       // });
       // 밑의 코드처럼 변경
      videoController!.seekTo(
        Duration(
          seconds: val.toInt(),
        ),
      );       
    },
    max: videoController!.value.duration.inSeconds
        .toDouble(),
    min: 0,
  ),

 그렇다면 플레이가 잘 되는것을 볼 수 있다.

 

6. 코드 정리 

...
_SliderBottom(
    currentPosition: currentPosition,
    maxPosition: videoController!.value.duration,
    onSliderChanged: onSliderChanged)
...              
void onSliderChanged(double val) {
    videoController!.seekTo(
      Duration(
        seconds: val.toInt(),
      ),
    );
  }
...
class _SliderBottom extends StatelessWidget {
  final Duration currentPosition;
  final Duration maxPosition;
  final ValueChanged<double> onSliderChanged;

  const _SliderBottom(
      {Key? key,
      required this.currentPosition,
      required this.maxPosition,
      required this.onSliderChanged})
      : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Positioned(
      bottom: 0,
      right: 0,
      left: 0,
      child: Padding(
        padding: const EdgeInsets.symmetric(horizontal: 8.0),
        child: Row(
          children: [
            Text(
              '${currentPosition.inMinutes}:${(currentPosition.inSeconds % 60).toString().padLeft(2, "0")}',
              style: TextStyle(
                color: Colors.white,
              ),
            ),
            Expanded(
              child: Slider(
                value: currentPosition.inSeconds.toDouble(),
                onChanged: onSliderChanged,
                max: maxPosition.inSeconds.toDouble(),
                min: 0,
              ),
            ),
            Text(
              '${maxPosition.inMinutes}:${(maxPosition.inSeconds % 60).toString().padLeft(2, "0")}',
              style: TextStyle(
                color: Colors.white,
              ),
            ),
          ],
        ),
      ),
    );
  }
}

버그 수정

상황: Row() 위젯에다가 CrossAxisAlignment.stretch 로 해놨기 떄문에 버튼의 위, 아래를 클릭해서 onPressed 가 된다.

 

7. 비디오 컨트롤하는 UI 를 탭했을때만 보이게 하기!

 

 

제스쳐로 감싸주고 boolean 변수로 값 담아서 분기처리 해주면 된다.

// 초기에는 false 로 안보이게 하는 boolean 변수 선언
bool showControls = false;

...

return AspectRatio(
	aspectRatio: videoController!.value.aspectRatio,
    child: GestureDetector(
    	onTap: (){
        	setState(() {
            	showControls = !showControls;
            })
        }
    ),
    child: Stack(
    	children: [
        ...
        // 분기처리 추가
        if(showControls){} 
        ]
    )
)

왼) 기본, 오) 탭 한후 

8. 새로운 비디오 불러오기 버튼 기능 만들기!

// home_screen.dart
  // 비디오 있을 때 뷰
  Widget renderVideo() {
    return CustomVideoPlayer(
      video: video!,
      onNewVideoPressed: onNewVideoPressed,
    );
  }
  // 기존에 로고클릭하면 나오는 함수 그대로 가져다 쓴다. 이름은 바꿔주자 onLogoTap -> onNewVideoPressed
  void onNewVideoPressed() async {
    // ImagePicker 를 통한 갤러리의 비디오 가져오기
    final video = await ImagePicker().pickVideo(source: ImageSource.gallery);

    if (video != null) {
      setState(() {
        this.video = video;
      });
    }
  }
  
  
// custom_video_player.dart
class CustomVideoPlayer extends StatefulWidget {
   final VoidCallback onNewVideoPressed;
    
   ...
   _NewVideo(onPressed: widget.onNewVideoPressed),
   ...
}

 

 

아이콘을 클릭하면 갤러리의 비디오를 선택할 수 있는 창이 정상적으로 잘 뜬다.

하지만, 새로운 동영상을 불러올수는 없다. 그 이유는!

videoController 가 생성되는 시점은 initState() 함수 안에서 존재한다.

initState 함수는 StatefulWidget 이 생성될때 딱! 한번 만 실행이 되기 때문에, 아무리 새로운 video 가 들어온다고 해도 initState() 가 실행이 되지 않으니, videoController 컨트롤 생성은 물론, 초기화가 되지 않는다.

그러니 해결방법은 StatefulWidget 의 생명 주기 할때 배운 State가 살아있는 상태에서 파라미터만 바뀌었을때(StatefulWidget 이 실행이 된적있는데, 파라미터만 변경이 되었을떄)! didUpdateWidget 이 호출이 된다고 배웠는데, 그 위젯안에 기존의 비디오 경로와 새로운 비디오 경로가 다르면 initializeConroller() 함수를 실행시켜주면 된다.

 

 

  // StatefulWidget 이 실행이 되었고, 파라미터값만 변경이 되었을때 didUpdateWidget 이 호출된다.
  @override
  void didUpdateWidget(covariant CustomVideoPlayer oldWidget) {
    super.didUpdateWidget(oldWidget);

    if(oldWidget.video.path != widget.video.path) {
      initializeController();
    }
  }

코드 추가

마지막으로 비디오를 보다가 새로운 영상을 추가해준다고 한다면, 새로추가한 영상의 postion 을 처음으로(0) 안돌려줘서 그렇다.

 // 코드 추가
 initializeController() async {
    // Duration() 으로 돌려놓는다.
    currentPosition = Duration();
    ...
 }

끝으로...

  1. 비디오 컨트롤러를 통해서 영상의 길이, 영상 플레이 중인지 아닌지, 이전버튼, 이후버튼 함수 컨트롤을 배웠다!
  2. didUpdateWidget 이 언제 실행이 되는 지 알 수 있다.(StatefulWidget 이 실행되고, 파라미터값이 바뀌었을때)
  3. Controller 와 State 안의 변수를 서로 연동시키는 방법을 배웠다.

 

728x90