Flutter 튜토리얼 21편: 비디오 재생

요약#

핵심 요지#

  • 문제 정의: 앱에서 비디오를 재생하려면 플랫폼별 네이티브 플레이어를 다뤄야 하고, 컨트롤 UI도 직접 만들어야 한다.
  • 핵심 주장: video_player1 패키지와 chewie2를 함께 사용하면 플랫폼 차이 없이 완성된 비디오 플레이어를 구현할 수 있다.
  • 주요 근거: VideoPlayerController3로 비디오를 로드하고, AspectRatioVideoPlayer 위젯으로 화면에 표시하며, Chewie가 컨트롤 UI를 제공한다.
  • 실무 기준: iOS 시뮬레이터에서는 에셋 비디오만 재생 가능하고, 네트워크 비디오는 실제 기기에서 테스트해야 한다.
  • 한계: Linux와 Windows는 video_player를 지원하지 않는다.

문서가 설명하는 범위#

  • VideoPlayerController 초기화와 생명주기 관리
  • 네트워크와 에셋 비디오 재생
  • 재생 컨트롤 구현 (재생/일시정지, 탐색)
  • Chewie로 완성된 UI 구현

읽는 시간: 12분 | 난이도: 중급


참고 자료#


문제 상황#

앱에서 비디오를 재생하는 것은 단순해 보이지만 여러 복잡한 문제가 있습니다.

비디오 재생의 어려움#

문제 1: 플랫폼별로 다른 네이티브 플레이어 (AVPlayer, ExoPlayer)
문제 2: 비디오 초기화는 비동기로 완료까지 시간 필요
문제 3: 메모리 관리 - 컨트롤러를 제대로 dispose하지 않으면 메모리 누수
문제 4: 재생 컨트롤 UI를 직접 만들어야 함

해결 방법#

Flutter의 video_player 패키지는 iOS에서 AVPlayer를, Android에서 ExoPlayer를 사용해서 비디오를 재생합니다. 통일된 API로 플랫폼 차이를 추상화합니다.

챕터 1: video_player 설정하기#

Why#

NOTE

비디오 재생 기능을 사용하려면 먼저 패키지를 설치하고 플랫폼별 권한을 설정해야 합니다.
네트워크 비디오를 재생하려면 인터넷 접근 권한이 필요합니다.

패키지 설치 → 권한 설정 → 컨트롤러 생성 → 비디오 재생

What#

NOTE

video_player 패키지는 iOS, Android, macOS, Web에서 비디오 재생을 지원합니다.
Linux와 Windows는 현재 지원하지 않습니다.

How#

TIP

패키지 설치

Terminal window
flutter pub add video_player

Android 권한 설정 (android/app/src/main/AndroidManifest.xml)

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application ...>
...
</application>
<!-- 네트워크 비디오 재생을 위해 필요 -->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

iOS 설정 (ios/Runner/Info.plist)

<key>NSAppTransportSecurity</key>
<dict>
<key>NSAllowsArbitraryLoads</key>
<true/>
</dict>

macOS 설정 (macos/Runner/DebugProfile.entitlements)

<key>com.apple.security.network.client</key>
<true/>

Watch out#

WARNING

iOS 시뮬레이터에서는 네트워크 비디오를 재생할 수 없습니다.
에셋 비디오만 테스트 가능하며, 네트워크 비디오는 실제 기기에서 테스트해야 합니다.

// 시뮬레이터에서 테스트할 때는 에셋 비디오 사용
VideoPlayerController.asset('assets/videos/sample.mp4')
// 실제 기기에서 네트워크 비디오 테스트
VideoPlayerController.networkUrl(Uri.parse('https://...'))

결론: video_player 패키지를 설치하고 플랫폼별 권한을 설정해야 비디오를 재생할 수 있습니다.


챕터 2: VideoPlayerController 초기화#

Why#

NOTE

비디오를 재생하려면 먼저 VideoPlayerController를 생성하고 초기화해야 합니다.
초기화는 비동기 작업이므로 완료될 때까지 기다려야 합니다.

graph LR A[Controller 생성] --> B[initialize 호출] B --> C[Future 완료 대기] C --> D[비디오 재생 가능]

What#

NOTE

VideoPlayerController는 비디오 소스(네트워크, 에셋, 파일)를 로드하고 재생을 제어합니다.
initialize()를 호출해서 비디오 메타데이터를 로드하고 재생 준비를 완료합니다.

How#

TIP

네트워크 비디오 초기화

import 'package:flutter/material.dart';
import 'package:video_player/video_player.dart';
class VideoScreen extends StatefulWidget {
const VideoScreen({super.key});
@override
State<VideoScreen> createState() => _VideoScreenState();
}
class _VideoScreenState extends State<VideoScreen> {
late VideoPlayerController _controller;
late Future<void> _initializeVideoPlayerFuture;
@override
void initState() {
super.initState();
// 네트워크 비디오 컨트롤러 생성
_controller = VideoPlayerController.networkUrl(
Uri.parse(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
),
);
// 초기화 Future 저장
_initializeVideoPlayerFuture = _controller.initialize();
}
@override
void dispose() {
// 반드시 dispose 호출
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return const Placeholder(); // 다음 챕터에서 구현
}
}

에셋 비디오 초기화

// pubspec.yaml에 에셋 등록 필요
// flutter:
// assets:
// - assets/videos/
_controller = VideoPlayerController.asset('assets/videos/sample.mp4');
_initializeVideoPlayerFuture = _controller.initialize();

파일 비디오 초기화 (웹 제외)

import 'dart:io';
_controller = VideoPlayerController.file(File('/path/to/video.mp4'));
_initializeVideoPlayerFuture = _controller.initialize();

Watch out#

WARNING

dispose()를 호출하지 않으면 메모리 누수가 발생합니다.
특히 리스트에서 여러 비디오를 다룰 때 주의해야 합니다.

@override
void dispose() {
// 반드시 컨트롤러 해제
_controller.dispose();
super.dispose();
}

초기화 전에 play()를 호출하면 아무 일도 일어나지 않습니다. 반드시 initialize() 완료 후에 재생을 시작해야 합니다.

결론: VideoPlayerController를 생성하고 initialize()가 완료된 후에 비디오를 재생할 수 있습니다.


챕터 3: 비디오 화면에 표시하기#

Why#

NOTE

초기화된 비디오를 화면에 표시하려면 VideoPlayer 위젯을 사용합니다.
비디오 비율을 유지하려면 AspectRatio 위젯으로 감싸야 합니다.

초기화 완료 확인 → AspectRatio로 비율 유지 → VideoPlayer로 표시

What#

NOTE

VideoPlayer 위젯은 VideoPlayerController의 비디오를 화면에 렌더링합니다.
FutureBuilder를 사용해서 초기화 완료 상태를 처리합니다.

How#

TIP

기본 비디오 표시

@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('비디오 플레이어')),
body: FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
// 초기화 완료 - 비디오 표시
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
} else {
// 초기화 중 - 로딩 표시
return const Center(
child: CircularProgressIndicator(),
);
}
},
),
);
}

에러 처리 추가

FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.hasError) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.error, size: 48, color: Colors.red),
const SizedBox(height: 16),
Text('비디오 로드 실패: ${snapshot.error}'),
],
),
);
}
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
}
return const Center(child: CircularProgressIndicator());
},
)

전체 화면 비디오

SizedBox.expand(
child: FittedBox(
fit: BoxFit.cover,
child: SizedBox(
width: _controller.value.size.width,
height: _controller.value.size.height,
child: VideoPlayer(_controller),
),
),
)

Watch out#

WARNING

AspectRatio를 사용하지 않으면 비디오가 찌그러지거나 부모 크기에 맞춰 늘어납니다.

// 잘못된 방법 - 비율이 깨질 수 있음
VideoPlayer(_controller)
// 올바른 방법 - 비율 유지
AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
)

_controller.value.aspectRatio는 초기화 후에만 올바른 값을 반환합니다. 초기화 전에는 1.0을 반환합니다.

결론: FutureBuilder로 초기화 상태를 확인하고, AspectRatio로 비율을 유지하면서 VideoPlayer를 표시합니다.


챕터 4: 재생 컨트롤 구현#

Why#

NOTE

사용자가 비디오를 제어하려면 재생/일시정지, 탐색, 볼륨 조절 등의 컨트롤이 필요합니다.
VideoPlayerController의 메서드로 이러한 기능을 구현합니다.

play() → 재생 시작
pause() → 일시정지
seekTo() → 특정 위치로 이동
setVolume() → 볼륨 조절
setLooping() → 반복 재생

What#

NOTE

VideoPlayerController는 재생 상태와 메타데이터를 VideoPlayerValue로 제공합니다.
isPlaying, position, duration 등의 속성으로 현재 상태를 확인할 수 있습니다.

How#

TIP

재생/일시정지 버튼

Scaffold(
body: FutureBuilder(
future: _initializeVideoPlayerFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
return AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
);
}
return const Center(child: CircularProgressIndicator());
},
),
floatingActionButton: FloatingActionButton(
onPressed: () {
setState(() {
if (_controller.value.isPlaying) {
_controller.pause();
} else {
_controller.play();
}
});
},
child: Icon(
_controller.value.isPlaying ? Icons.pause : Icons.play_arrow,
),
),
)

진행률 표시기와 탐색

Column(
children: [
AspectRatio(
aspectRatio: _controller.value.aspectRatio,
child: VideoPlayer(_controller),
),
VideoProgressIndicator(
_controller,
allowScrubbing: true, // 드래그로 탐색 허용
colors: const VideoProgressColors(
playedColor: Colors.blue,
bufferedColor: Colors.grey,
backgroundColor: Colors.black26,
),
),
],
)

커스텀 컨트롤 바

class VideoControlBar extends StatelessWidget {
final VideoPlayerController controller;
const VideoControlBar({super.key, required this.controller});
@override
Widget build(BuildContext context) {
return ValueListenableBuilder<VideoPlayerValue>(
valueListenable: controller,
builder: (context, value, child) {
return Row(
children: [
// 재생/일시정지
IconButton(
icon: Icon(value.isPlaying ? Icons.pause : Icons.play_arrow),
onPressed: () {
value.isPlaying ? controller.pause() : controller.play();
},
),
// 현재 시간
Text(_formatDuration(value.position)),
// 진행률 바
Expanded(
child: Slider(
value: value.position.inMilliseconds.toDouble(),
min: 0,
max: value.duration.inMilliseconds.toDouble(),
onChanged: (newValue) {
controller.seekTo(Duration(milliseconds: newValue.toInt()));
},
),
),
// 전체 시간
Text(_formatDuration(value.duration)),
// 볼륨
IconButton(
icon: Icon(value.volume > 0 ? Icons.volume_up : Icons.volume_off),
onPressed: () {
controller.setVolume(value.volume > 0 ? 0 : 1);
},
),
],
);
},
);
}
String _formatDuration(Duration duration) {
final minutes = duration.inMinutes.remainder(60).toString().padLeft(2, '0');
final seconds = duration.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$minutes:$seconds';
}
}

Watch out#

WARNING

setState()를 자주 호출하면 성능이 저하됩니다.
재생 중에 매 프레임마다 위치를 업데이트하면 비효율적입니다.

// 비효율적 - 매 프레임 setState
_controller.addListener(() {
setState(() {}); // 전체 위젯 리빌드
});
// 효율적 - ValueListenableBuilder 사용
ValueListenableBuilder<VideoPlayerValue>(
valueListenable: _controller,
builder: (context, value, child) {
return Text('${value.position}'); // 해당 부분만 리빌드
},
)

결론: VideoPlayerController의 메서드로 재생을 제어하고, ValueListenableBuilder로 효율적으로 UI를 업데이트합니다.


챕터 5: Chewie로 완성된 UI 구현#

Why#

NOTE

직접 컨트롤 UI를 만드는 것은 시간이 많이 걸립니다.
chewie 패키지는 Material/Cupertino 스타일의 완성된 비디오 플레이어 UI를 제공합니다.

video_player → 비디오 재생 기능
chewie → 완성된 컨트롤 UI

What#

NOTE

Chewie는 video_player 위에 구축된 UI 패키지입니다.
재생 버튼, 진행률 바, 전체 화면 전환, 자막 등을 기본 제공합니다.

How#

TIP

패키지 설치

Terminal window
flutter pub add chewie

기본 사용법

import 'package:chewie/chewie.dart';
import 'package:video_player/video_player.dart';
class ChewieVideoPlayer extends StatefulWidget {
const ChewieVideoPlayer({super.key});
@override
State<ChewieVideoPlayer> createState() => _ChewieVideoPlayerState();
}
class _ChewieVideoPlayerState extends State<ChewieVideoPlayer> {
late VideoPlayerController _videoPlayerController;
ChewieController? _chewieController;
@override
void initState() {
super.initState();
_initializePlayer();
}
Future<void> _initializePlayer() async {
_videoPlayerController = VideoPlayerController.networkUrl(
Uri.parse(
'https://flutter.github.io/assets-for-api-docs/assets/videos/butterfly.mp4',
),
);
await _videoPlayerController.initialize();
_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoPlay: true,
looping: true,
);
setState(() {});
}
@override
void dispose() {
_videoPlayerController.dispose();
_chewieController?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Chewie 플레이어')),
body: Center(
child: _chewieController != null &&
_chewieController!.videoPlayerController.value.isInitialized
? Chewie(controller: _chewieController!)
: const CircularProgressIndicator(),
),
);
}
}

ChewieController 옵션

_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoPlay: false, // 자동 재생
looping: true, // 반복 재생
aspectRatio: 16 / 9, // 비율 고정
autoInitialize: true, // 자동 초기화
showControls: true, // 컨트롤 표시
showOptions: true, // 설정 메뉴 표시
allowFullScreen: true, // 전체 화면 허용
allowMuting: true, // 음소거 허용
allowPlaybackSpeedChanging: true, // 재생 속도 변경
playbackSpeeds: [0.5, 1.0, 1.5, 2.0],
materialProgressColors: ChewieProgressColors(
playedColor: Colors.red,
handleColor: Colors.red,
backgroundColor: Colors.grey,
bufferedColor: Colors.white54,
),
);

Watch out#

WARNING

Chewie를 사용할 때 VideoPlayerControllerChewieController 둘 다 dispose해야 합니다.

@override
void dispose() {
_videoPlayerController.dispose(); // 먼저 비디오 컨트롤러
_chewieController?.dispose(); // 그 다음 Chewie 컨트롤러
super.dispose();
}

autoInitialize: true를 설정하면 별도로 initialize()를 호출할 필요가 없지만, 초기화 완료 시점을 정확히 알기 어렵습니다.

결론: Chewie를 사용하면 완성된 비디오 플레이어 UI를 쉽게 구현할 수 있습니다.


챕터 6: 자막과 추가 기능#

Why#

NOTE

비디오에 자막을 추가하면 접근성이 향상됩니다.
Chewie는 자막, 재생 속도 변경, 커스텀 옵션 등을 지원합니다.

자막 → 청각 장애인 지원, 다국어 지원
재생 속도 → 학습 영상에서 유용
커스텀 옵션 → 화질 선택 등

What#

NOTE

Subtitle 클래스로 시작 시간, 종료 시간, 텍스트를 정의합니다.
ChewieControllersubtitle 속성에 자막 목록을 전달합니다.

How#

TIP

자막 추가

_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
autoPlay: true,
subtitle: Subtitles([
Subtitle(
index: 0,
start: Duration.zero,
end: const Duration(seconds: 5),
text: '안녕하세요, Flutter 비디오 튜토리얼입니다.',
),
Subtitle(
index: 1,
start: const Duration(seconds: 5),
end: const Duration(seconds: 10),
text: '이 영상에서는 비디오 재생 방법을 배웁니다.',
),
Subtitle(
index: 2,
start: const Duration(seconds: 10),
end: const Duration(seconds: 15),
text: '시작해볼까요?',
),
]),
showSubtitles: true, // 자막 기본 표시
subtitleBuilder: (context, subtitle) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.black54,
borderRadius: BorderRadius.circular(4),
),
child: Text(
subtitle,
style: const TextStyle(
color: Colors.white,
fontSize: 16,
),
),
);
},
);

커스텀 옵션 추가

_chewieController = ChewieController(
videoPlayerController: _videoPlayerController,
additionalOptions: (context) {
return [
OptionItem(
onTap: () => _showQualityDialog(context),
iconData: Icons.high_quality,
title: '화질 선택',
),
OptionItem(
onTap: () => _downloadVideo(),
iconData: Icons.download,
title: '다운로드',
),
];
},
);

SRT 파일에서 자막 로드

Future<Subtitles> _loadSubtitlesFromSrt(String srtPath) async {
final srtContent = await rootBundle.loadString(srtPath);
final subtitles = <Subtitle>[];
final blocks = srtContent.split('\n\n');
for (var i = 0; i < blocks.length; i++) {
final lines = blocks[i].split('\n');
if (lines.length >= 3) {
final times = lines[1].split(' --> ');
subtitles.add(Subtitle(
index: i,
start: _parseSrtTime(times[0]),
end: _parseSrtTime(times[1]),
text: lines.sublist(2).join('\n'),
));
}
}
return Subtitles(subtitles);
}
Duration _parseSrtTime(String time) {
final parts = time.split(':');
final seconds = parts[2].split(',');
return Duration(
hours: int.parse(parts[0]),
minutes: int.parse(parts[1]),
seconds: int.parse(seconds[0]),
milliseconds: int.parse(seconds[1]),
);
}

Watch out#

WARNING

자막 타이밍이 정확하지 않으면 사용자 경험이 나빠집니다.
비디오와 자막 시간을 정확히 맞춰야 합니다.

// 자막 시작/종료 시간이 겹치지 않도록 주의
Subtitle(
index: 0,
start: Duration.zero,
end: const Duration(seconds: 5), // 5초에서 끝
text: '첫 번째 자막',
),
Subtitle(
index: 1,
start: const Duration(seconds: 5), // 5초에서 시작 (겹치지 않음)
end: const Duration(seconds: 10),
text: '두 번째 자막',
),

결론: Chewie의 자막과 추가 옵션 기능을 활용하면 전문적인 비디오 플레이어를 구현할 수 있습니다.


한계#

Flutter의 video_player는 대부분의 상황을 처리하지만 몇 가지 제약이 있습니다.

  • 플랫폼 지원: Linux와 Windows는 지원하지 않습니다.
  • DRM 미지원: 기본 패키지는 DRM 보호 콘텐츠를 재생할 수 없습니다.
  • 코덱 제한: 플랫폼별로 지원하는 코덱이 다릅니다.
  • 백그라운드 재생: 기본적으로 백그라운드 재생을 지원하지 않습니다.
  • 스트리밍 제한: HLS/DASH 스트리밍은 플랫폼별 지원이 다릅니다.

Footnotes#

  1. video_player: Flutter에서 비디오를 재생하는 공식 플러그인이다. iOS의 AVPlayer와 Android의 ExoPlayer를 사용한다.

  2. chewie: video_player 위에 구축된 UI 패키지로, Material/Cupertino 스타일의 완성된 플레이어 UI를 제공한다.

  3. VideoPlayerController: 비디오 소스를 로드하고 재생을 제어하는 컨트롤러 클래스다.

공유

이 글이 도움이 되었다면 다른 사람과 공유해주세요!

Flutter 튜토리얼 21편: 비디오 재생
https://moodturnpost.net/posts/flutter/flutter-video-playback/
작성자
Moodturn
게시일
2026-01-08
Moodturn

목차