본문 바로가기
Flutter for Beginners

Flutter 비디오 플레이어

by Andrew's Akashic Records 2025. 4. 10.
728x90

비디오 플레이어에 사용되는 패키지

video_player

Flutter에서 동영상 재생 기능을 구현할 수 있게 해주는 공식 패키지입니다.

video_player 자체에는 전체화면 기능이 없고, 전체화면/회전/시스템 UI 숨김은 Flutter의 SystemChrome, MediaQuery 등을 통해 직접 구현해야 합니다.

1. 주요 기능

  • 파일 동영상 재생: 로컬 디바이스에 저장된 mp4 등
  • 네트워크 동영상 재생: HTTP/HTTPS URL (YouTube 스트림 등은 불가)
  • 재생 / 일시정지 / 탐색(seek) 제어
  • 음소거 / 볼륨 / 반복재생 지원
  • 전체화면 모드 직접 구현 가능
  • 비디오 재생 위치 추적 가능
  • 기본적인 UI는 제공 안 함 → 사용자가 직접 구성해야 함
항목 내용
패키지명 video_player
버전 ^2.9.5 (2024년 기준 최신 중 하나)
지원 플랫폼 Android, iOS, Web, macOS, Windows, Linux
개발 주체 Flutter 공식 팀 (Google)
주요 용도 앱 내에서 동영상 파일 또는 네트워크 스트리밍 재생 가능

video_player 2.9.5의 특징

  • 안정성과 성능 향상 (이전 버전 대비)
  • 더 넓은 플랫폼 지원: Android, iOS, Web, macOS 등
  • iOS AVPlayer 기반, Android ExoPlayer 기반의 네이티브 성능
  • Web에서도 작동하지만 Web에서는 제한사항이 있음

2. 지원하는 동영상 소스 타입

// 네트워크 URL
VideoPlayerController.network('https://example.com/video.mp4');

// 앱에 포함된 asset
VideoPlayerController.asset('assets/videos/sample.mp4');

// 로컬 파일
VideoPlayerController.file(File('/storage/emulated/0/Movies/video.mp4'));

3. 기본 사용 예시 코드

late VideoPlayerController _controller;

@override
void initState() {
  super.initState();
  _controller = VideoPlayerController.network('https://...')
    ..initialize().then((_) {
      setState(() {}); // 준비되면 화면 갱신
    });
}

4. 언제 video_player를 쓰면 좋을까?

상황 적합도
앱에서 간단한 동영상 보여주기 아주 적합
커스텀 제어 UI 만들고 싶을 때 강력함
YouTube 등 보호된 영상 스트리밍 다른 API 필요 (예: youtube_player_flutter)

file_picker

Flutter 앱에서 파일 탐색기(File Explorer)를 열어 사용자가 파일을 선택할 수 있게 해주는 대표적인 패키지입니다. Flutter 공식 패키지는 아니지만, 널리 사용되는 신뢰성 높은 패키지 다양한 플랫폼에서 사용자가 원하는 파일을 자유롭게 선택하게 해주는
강력하고 유연한 Flutter 파일 선택기 라이브러리입니다.

항목 내용
패키지명 file_picker
최신 안정 버전 (2024년 기준) ^10.0.0
지원 플랫폼 Android, iOS, macOS, Windows, Linux, Web
주요 기능 파일 선택, 다중 선택, 폴더 선택, 저장 경로 선택 등

1. 주요 기능 요약

  • 단일 파일 선택 하나의 파일 선택 (예: 이미지, 동영상, 문서 등)
  • 폴더 선택 전체 폴더 선택 (지원되는 플랫폼에서)
  • 다중 파일 선택 여러 파일 한꺼번에 선택 가능
  • 파일 타입 필터 이미지, 동영상, 문서 등 특정 타입만 선택 가능
  • 저장 경로 선택 파일을 저장할 디렉토리 경로 선택 가능
  • 웹 지원 Flutter Web에서도 사용 가능 (제한 있음)

2. 사용 예시

2.1 단일 파일 선택 (예: 동영상)

final result = await FilePicker.platform.pickFiles(
  type: FileType.video,
);
if (result != null && result.files.single.path != null) {
  File file = File(result.files.single.path!);
  // 사용: file.path 등
}

2.2. 이미지 여러 개 선택

final result = await FilePicker.platform.pickFiles(
  allowMultiple: true,
  type: FileType.image,
);

2.3. 모든 형식 허용

final result = await FilePicker.platform.pickFiles(
  type: FileType.any,
);

2.4. 확장자 직접 지정 (예: .pdf, .docx 등)

final result = await FilePicker.platform.pickFiles(
  type: FileType.custom,
  allowedExtensions: ['pdf', 'docx', 'xlsx'],
);

3. 주요 클래스 및 속성

항목 설명
FilePickerResult 선택된 파일 정보가 들어있는 객체
PlatformFile 각 파일의 정보 (이름, 경로, 크기 등)
FileType 선택할 파일 형식 지정 (video, image, custom 등)
FilePicker.platform 현재 플랫폼에서 동작하는 파일 피커 인스턴스

4. Android & iOS 설정

4.1. Android: AndroidManifest.xml

<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" android:maxSdkVersion="28"/>

 

Android 13(API 33)+부터는 Storage 권한 변경됨 → Flutter 문서 참조

 

4.2. iOS: Info.plist

<key>NSPhotoLibraryUsageDescription</key>
<string>사진 접근을 허용해주세요</string>
<key>NSDocumentsFolderUsageDescription</key>
<string>문서에 접근합니다</string>

4.3. Web 지원 참고

  • Web에서는 파일을 선택할 수 있지만, 경로 정보(file.path)는 제공되지 않음
  • 대신 Uint8List 로 파일 데이터를 받아야 함

비디오 플레이어 코드

비디오 플레이어 실행, 플레이 이미지 클릭 후 파일 선택

전체 코드

1. pubspec.yaml

dependencies:
  flutter:
    sdk: flutter
  video_player: ^2.9.5
  file_picker: ^10.0.0

2. /screen/video-player-screen.dart

import 'dart:async';
import 'dart:io';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
import 'package:flutter/services.dart';

// 동영상 플레이어 화면을 위한 Stateful 위젯
class VideoPlayerScreen extends StatefulWidget {
  const VideoPlayerScreen({super.key});

  @override
  _VideoPlayerScreenState createState() => _VideoPlayerScreenState();
}

class _VideoPlayerScreenState extends State<VideoPlayerScreen> {
  VideoPlayerController? _controller;  // 동영상 제어 컨트롤러
  bool _showControls = false;          // 제어 버튼 UI 표시 여부
  Timer? _hideTimer;                   // 제어 버튼 자동 숨김 타이머
  bool _isFullScreen = false;         // 전체화면 모드 여부

  @override
  void dispose() {
    // 상태 종료 시 컨트롤러 및 타이머 해제
    _controller?.dispose();
    _hideTimer?.cancel();

    // 전체화면 설정 복원 (세로 모드로)
    SystemChrome.setPreferredOrientations([
      DeviceOrientation.portraitUp,
    ]);
    SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
    super.dispose();
  }

  // 사용자가 동영상을 선택할 수 있도록 파일 피커 호출
  void _pickVideo() async {
    final result = await FilePicker.platform.pickFiles(type: FileType.video);
    if (result != null && result.files.single.path != null) {
      // 이전 컨트롤러 제거
      _controller?.dispose();

      // 새 동영상 컨트롤러 생성 및 초기화
      _controller = VideoPlayerController.file(
        File(result.files.single.path!),
      )..initialize().then((_) {
        setState(() {});  // UI 업데이트
        _controller?.play();  // 자동 재생
      });
    }
  }

  // 화면 탭 시 제어 버튼을 보여주고 5초 뒤에 자동 숨김
  void _toggleControls() {
    setState(() {
      _showControls = true;
    });

    _hideTimer?.cancel();
    _hideTimer = Timer(Duration(seconds: 5), () {
      setState(() {
        _showControls = false;
      });
    });
  }

  // 전체화면 모드 전환
  void _toggleFullScreen() async {
    if (_isFullScreen) {
      // 전체화면 종료: 세로 모드 및 시스템 UI 복원
      await SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
      await SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
    } else {
      // 전체화면 진입: 가로 모드 및 시스템 UI 숨김
      await SystemChrome.setPreferredOrientations([
        DeviceOrientation.landscapeLeft,
        DeviceOrientation.landscapeRight,
      ]);
      await SystemChrome.setEnabledSystemUIMode(SystemUiMode.immersiveSticky);
    }

    setState(() {
      _isFullScreen = !_isFullScreen;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      backgroundColor: Colors.black,
      body: GestureDetector(
        onTap: _toggleControls, // 화면 탭 시 제어 UI 토글
        child: SizedBox.expand(  // 전체 화면을 차지
          child: _controller == null
              ? _buildLogoScreen()
              : _controller!.value.isInitialized
              ? Stack(
            alignment: Alignment.bottomCenter,
            children: [
              Center(
                child: FittedBox(
                  fit: BoxFit.contain, // 비율 유지하면서 화면에 맞춤
                  child: SizedBox(
                    width: _controller!.value.size.width,
                    height: _controller!.value.size.height,
                    child: VideoPlayer(_controller!),
                  ),
                ),
              ),
              if (_showControls) _buildControls(),
            ],
          )
              : Center(child: CircularProgressIndicator()),

        ),
      ),
    );
  }

  // 초기 로고 화면: 동영상이 선택되지 않았을 때 표시
  Widget _buildLogoScreen() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.center,
      children: [
        GestureDetector(
          onTap: _pickVideo,  // 로고 클릭 시 동영상 선택
          child: Icon(Icons.play_circle_fill, size: 100, color: Colors.white),
        ),
        SizedBox(height: 20),
        Text(
          'Andrew Player',
          style: TextStyle(color: Colors.white, fontSize: 24),
        ),
      ],
    );
  }

  // 동영상 재생 제어 버튼 UI
  Widget _buildControls() {
    return Column(
      mainAxisAlignment: MainAxisAlignment.end,
      children: [
        // 하단 동영상 진행 막대
        VideoProgressIndicator(_controller!, allowScrubbing: true),

        // 버튼 그룹: 뒤로가기, 10초 이전, 재생/일시정지, 10초 이후, 전체화면
        Row(
          mainAxisAlignment: MainAxisAlignment.center,
          children: [
            // 앱 홈으로 돌아가기
            IconButton(
              icon: Icon(Icons.arrow_back, color: Colors.white),
              onPressed: () {
                setState(() {
                  _controller?.pause();     // 재생 중지
                  _controller?.dispose();   // 메모리 해제
                  _controller = null;       // 초기화
                });

                // 전체화면 상태 복원
                SystemChrome.setPreferredOrientations([DeviceOrientation.portraitUp]);
                SystemChrome.setEnabledSystemUIMode(SystemUiMode.edgeToEdge);
                _isFullScreen = false;
              },
            ),

            // 10초 이전으로 이동
            IconButton(
              icon: Icon(Icons.replay_10, color: Colors.white),
              onPressed: () {
                final newPos = _controller!.value.position - Duration(seconds: 10);
                _controller!.seekTo(newPos > Duration.zero ? newPos : Duration.zero);
              },
            ),

            // 재생/일시정지 토글
            IconButton(
              icon: Icon(
                _controller!.value.isPlaying ? Icons.pause : Icons.play_arrow,
                color: Colors.white,
              ),
              onPressed: () {
                setState(() {
                  _controller!.value.isPlaying
                      ? _controller!.pause()
                      : _controller!.play();
                });
              },
            ),

            // 10초 이후로 이동
            IconButton(
              icon: Icon(Icons.forward_10, color: Colors.white),
              onPressed: () {
                final newPos = _controller!.value.position + Duration(seconds: 10);
                if (newPos < _controller!.value.duration) {
                  _controller!.seekTo(newPos);
                }
              },
            ),

            // 전체화면 토글 버튼
            IconButton(
              icon: Icon(
                _isFullScreen ? Icons.fullscreen_exit : Icons.fullscreen,
                color: Colors.white,
              ),
              onPressed: _toggleFullScreen,
            ),
          ],
        ),

        SizedBox(height: 20),
      ],
    );
  }
}

3. main.dart

import 'package:flutter/material.dart';
import 'package:andrew_player/screen/video_player_screen.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Andrew Player',
      home: VideoPlayerScreen(),
      debugShowCheckedModeBanner: false,
    );
  }
}

추가: 동영상이 찌그러지지 않고 자연스럽게 화면에 표시되도록 하기

  • AspectRatio: 비율 유지에는 좋지만, 부모의 제약을 무시하면 찌그러질 수 있음.
  • FittedBox + SizedBox: 부모 제약 안에서 비율 유지 + 크기 자동 조절, 더 안정적이고 실용적.

1. AspectRatio란?

AspectRatio(
  aspectRatio: 16 / 9,
  child: ...
)
  • child 위젯의 비율(aspect ratio) 을 강제로 유지하게 해주는 위젯.
  • 비율만 맞추면 되기 때문에 부모 컨테이너의 크기와 맞지 않으면:
    • 여백이 생기거나
    • 강제로 늘어나거나 해서 찌그러질 수 있음.

2. FittedBox + SizedBox 조합이란?

FittedBox(
  fit: BoxFit.contain,
  child: SizedBox(
    width: videoWidth,
    height: videoHeight,
    child: VideoPlayer(_controller!),
  ),
)
  • FittedBox 는 자식(SizedBox)의 크기를 부모에 맞게 비율 유지하며 조절해줌
  • BoxFit.contain 은 "가능한 한 꽉 채우되, 찌그러지지 않게!" 라는 뜻
  • SizedBox 에 실제 동영상의 해상도(videoWidth, videoHeight)를 지정하면, FittedBox 가 그 크기를 기준으로 화면 안에 알맞게 보여줌
항목 AspectRatio  FittedBox + SizedBox
사용 목적 비율 고정 부모 안에서 콘텐츠 맞추기
실제 크기 기준 사용 단순 비율 실제 영상의 width/height 사용
화면 찌그러짐 가능성 있음 (부모 크기와 맞지 않으면) 없음 (fit 옵션으로 대응 가능)
비율 유지 O O (BoxFit.contain일 때)
전체화면 대응 제한적 우수
728x90