SMALL
플레이어의 UI 및 기능은 출처 링크에서 가져와서 실행하였다
JustAudioExample 클래스는 음원을 url로 제공하여 스트리밍 재생을 할 수 있도록 지원하는 클래스이다
StreamPlayer 클래스는 JustAudioExample 클래스를 5개의 음원 확장자(mp3, wav, opus, ogg, flac) 중 하나를
선택하여 재생할 수 있도록 확장한 클래스이다
main.dart
import 'package:flutter/material.dart';
import 'JustAudioExample.dart';
import 'StreamPlayer.dart';
void main() {
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return const MaterialApp(
debugShowCheckedModeBanner: false,
title: 'Flutter Just Audio',
// home: JustAudioExample(),
home: StreamPlayer(),
);
}
}
class PositionData {
final Duration position;
final Duration bufferedPosition;
final Duration duration;
PositionData(this.position, this.bufferedPosition, this.duration);
}
SeekBar.dart
// The code below can be found in
// just_audio github repository
import 'dart:math';
import 'package:flutter/material.dart';
import 'HiddenThumbComponentShape.dart';
class SeekBar extends StatefulWidget {
final Duration duration;
final Duration position;
final Duration bufferedPosition;
final ValueChanged<Duration>? onChanged;
final ValueChanged<Duration>? onChangeEnd;
const SeekBar({
Key? key,
required this.duration,
required this.position,
required this.bufferedPosition,
this.onChanged,
this.onChangeEnd,
}) : super(key: key);
@override
SeekBarState createState() => SeekBarState();
}
class SeekBarState extends State<SeekBar> {
double? _dragValue;
late SliderThemeData _sliderThemeData;
@override
void didChangeDependencies() {
super.didChangeDependencies();
_sliderThemeData = SliderTheme.of(context).copyWith(
trackHeight: 2.0,
);
}
@override
Widget build(BuildContext context) {
return Stack(
children: [
SliderTheme(
data: _sliderThemeData.copyWith(
thumbShape: HiddenThumbComponentShape(),
activeTrackColor: Colors.blue.shade100,
inactiveTrackColor: Colors.grey.shade300,
),
child: ExcludeSemantics(
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(widget.bufferedPosition.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
),
SliderTheme(
data: _sliderThemeData.copyWith(
inactiveTrackColor: Colors.transparent,
),
child: Slider(
min: 0.0,
max: widget.duration.inMilliseconds.toDouble(),
value: min(_dragValue ?? widget.position.inMilliseconds.toDouble(),
widget.duration.inMilliseconds.toDouble()),
onChanged: (value) {
setState(() {
_dragValue = value;
});
if (widget.onChanged != null) {
widget.onChanged!(Duration(milliseconds: value.round()));
}
},
onChangeEnd: (value) {
if (widget.onChangeEnd != null) {
widget.onChangeEnd!(Duration(milliseconds: value.round()));
}
_dragValue = null;
},
),
),
],
);
}
}
HiddenThumbComponentShape.dart
import 'package:flutter/material.dart';
class HiddenThumbComponentShape extends SliderComponentShape {
@override
Size getPreferredSize(bool isEnabled, bool isDiscrete) => Size.zero;
@override
void paint(
PaintingContext context,
Offset center, {
required Animation<double> activationAnimation,
required Animation<double> enableAnimation,
required bool isDiscrete,
required TextPainter labelPainter,
required RenderBox parentBox,
required SliderThemeData sliderTheme,
required TextDirection textDirection,
required double value,
required double textScaleFactor,
required Size sizeWithOverflow,
}) {}
}
JustAudioExample.dart
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:just_audio_player/main.dart';
import 'package:rxdart/rxdart.dart';
import 'SeekBar.dart';
class JustAudioExample extends StatefulWidget {
const JustAudioExample({super.key});
@override
State<JustAudioExample> createState() => _JustAudioExampleState();
}
class _JustAudioExampleState extends State<JustAudioExample> {
String imgUrl =
'https://firebasestorage.googleapis.com/v0/b/new-ml-6c02d.appspot.com/o/lessonAssets%2Fcs3%2Fch7%2Fls4%2Fearth.jpeg?alt=media&token=b9ce6139-5e08-495d-b74f-9dfce09e86e2';
String url =
'https://files.freemusicarchive.org//storage-freemusicarchive-org//tracks//CAsMyXsiK0RkmsBG2K75J4wdewYDJElKJCe1tSQM.mp3';
// Declare AudioPlayer variable
late AudioPlayer player;
bool isPlaying = false;
double volume = 0.5;
bool isVolumeDisabled = false;
@override
void initState() {
super.initState();
_initialize();
}
void _initialize() async {
// Instantiate AudioPlayer class
player = AudioPlayer();
// Set the audio url
await player.setUrl(url);
// Set the initial volume
await player.setVolume(volume);
setState(() {});
}
@override
void dispose() {
player.dispose();
super.dispose();
}
void _disableVolume() async {
// Set volume to 0
await player.setVolume(0);
setState(() {
if (volume > 0) {
isVolumeDisabled = true;
}
});
}
void _activateVolume() async {
// Set volume to previous value before it was 0
await player.setVolume(volume);
setState(() {
isVolumeDisabled = false;
});
}
IconData _getVolumeIcon() {
return (player.volume == 0) ? Icons.volume_off : Icons.volume_up_rounded;
}
void _playAudio() async {
setState(() {
isPlaying = true;
});
// Play the audio
await player.play();
}
void _pauseAudio() async {
setState(() {
isPlaying = false;
});
// Pause the audio
await player.pause();
}
IconData _getPlayPauseIcon() {
return (isPlaying) ? Icons.pause : Icons.play_arrow;
}
// Listen for current audio play position,
// Listen for buffer position,
// Listen for max audio length
Stream<PositionData> get _positionDataStream =>
Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
player.positionStream,
player.bufferedPositionStream,
player.durationStream,
(position, bufferedPosition, duration) => PositionData(
position, bufferedPosition, duration ?? Duration.zero));
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Just Audio Example'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 200,
width: 300,
child: Image.network(imgUrl),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
switch (isPlaying) {
case true:
return _pauseAudio();
default:
return _playAudio();
}
},
icon: Icon(_getPlayPauseIcon(), size: 50, color: Colors.white),
),
],
),
const SizedBox(height: 16),
StreamBuilder<PositionData>(
stream: _positionDataStream,
builder: (context, snapshot) {
final positionData = snapshot.data;
Duration remaining = (positionData?.duration != null &&
positionData?.position != null)
? positionData!.duration - positionData.position
: Duration.zero;
return Row(
children: [
Expanded(
child: SeekBar(
duration: positionData?.duration ?? Duration.zero,
position: positionData?.position ?? Duration.zero,
bufferedPosition:
positionData?.bufferedPosition ?? Duration.zero,
onChangeEnd: player.seek,
),
),
Text(
RegExp(r'((^0*[1-9]d*:)?d{2}:d{2}).d+$')
.firstMatch("$remaining")
?.group(1) ??
'$remaining',
style: Theme.of(context)
.textTheme
.caption
?.copyWith(color: Colors.white),
),
],
);
},
),
const SizedBox(height: 16),
Row(
children: [
IconButton(
onPressed: () {
switch (isVolumeDisabled) {
case true:
return _activateVolume();
default:
return _disableVolume();
}
},
icon: Icon(_getVolumeIcon(), size: 30, color: Colors.white),
),
Expanded(
child: Slider(
value: player.volume,
max: 1,
min: 0,
onChanged: (value) async {
setState(() {
volume = value;
});
await player.setVolume(value);
},
),
),
],
),
],
),
);
}
}
StreamPlayer.dart
import 'package:flutter/material.dart';
import 'package:just_audio/just_audio.dart';
import 'package:just_audio_player/main.dart';
import 'package:rxdart/rxdart.dart';
import 'SeekBar.dart';
class StreamPlayer extends StatefulWidget {
const StreamPlayer({super.key});
@override
State<StreamPlayer> createState() => _StreamPlayerState();
}
class _StreamPlayerState extends State<StreamPlayer> {
String imgUrl =
'https://firebasestorage.googleapis.com/v0/b/new-ml-6c02d.appspot.com/o/lessonAssets%2Fcs3%2Fch7%2Fls4%2Fearth.jpeg?alt=media&token=b9ce6139-5e08-495d-b74f-9dfce09e86e2';
// Google Drive에서 생성한 직접 다운로드 링크
String mp3Url = 'https://drive.google.com/uc?export=download&id=1SlS1Xn7r9sfgAtZOMxMxwB0P6_IeAX6s';
String wavUrl = "https://drive.google.com/uc?export=download&id=1oyoDu3pbG_PSVBA1Fd1dmZmVPrUNgMWv";
String opusUrl = "https://drive.google.com/uc?export=download&id=10UI16A-VLFzndCYp7YTyhwWXPknN8b6s";
String oggUrl = "https://drive.google.com/uc?export=download&id=1arq0BfPyXqbjDRQrcEHr_jHotg-cUyiD";
String flacUrl = "https://drive.google.com/uc?export=download&id=1LS06sSZ3eNSpW2Jz41DbSLbR3IgAAhrV";
late AudioPlayer player;
bool isPlaying = false;
double volume = 0.5;
bool isVolumeDisabled = false;
//dropdown button
///도메인(domain) : mp3, wav, opus, ogg, flac
String currentFileExtension = "mp3";
@override
void initState() {
super.initState();
player = AudioPlayer();
_initialize("mp3");
}
void _initialize(String extension) async {
// 기존에 재생 중인 오디오를 정지
await player.stop();
String url = "";
switch(extension) {
case "mp3":
url = mp3Url;
break;
case "wav":
url = mp3Url;
break;
case "opus":
url = mp3Url;
break;
case "ogg":
url = mp3Url;
break;
case "flac":
url = mp3Url;
break;
default:
url = mp3Url;
}
await player.setUrl(url); // 스트리밍 URL 설정
await player.setVolume(volume);
setState(() {
isPlaying = false; // 새로운 URL 설정 후 재생 상태 초기화
});
}
@override
void dispose() {
player.dispose();
super.dispose();
}
void _disableVolume() async {
await player.setVolume(0);
setState(() {
if (volume > 0) {
isVolumeDisabled = true;
}
});
}
void _activateVolume() async {
await player.setVolume(volume);
setState(() {
isVolumeDisabled = false;
});
}
IconData _getVolumeIcon() {
return (player.volume == 0) ? Icons.volume_off : Icons.volume_up_rounded;
}
void _playAudio() async {
setState(() {
isPlaying = true;
});
await player.play();
}
void _pauseAudio() async {
setState(() {
isPlaying = false;
});
await player.pause();
}
IconData _getPlayPauseIcon() {
return (player.playing) ? Icons.pause : Icons.play_arrow;
}
Stream<PositionData> get _positionDataStream =>
Rx.combineLatest3<Duration, Duration, Duration?, PositionData>(
player.positionStream,
player.bufferedPositionStream,
player.durationStream,
(position, bufferedPosition, duration) => PositionData(
position, bufferedPosition, duration ?? Duration.zero));
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Just Audio Example'),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
SizedBox(
height: 200,
width: 300,
child: Image.network(imgUrl),
),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
onPressed: () {
switch (isPlaying) {
case true:
return _pauseAudio();
default:
return _playAudio();
}
},
icon: Icon(_getPlayPauseIcon(), size: 50, color: Colors.white),
),
],
),
const SizedBox(height: 16),
StreamBuilder<PositionData>(
stream: _positionDataStream,
builder: (context, snapshot) {
final positionData = snapshot.data;
Duration remaining = (positionData?.duration != null &&
positionData?.position != null)
? positionData!.duration - positionData.position
: Duration.zero;
return Row(
children: [
Expanded(
child: SeekBar(
duration: positionData?.duration ?? Duration.zero,
position: positionData?.position ?? Duration.zero,
bufferedPosition:
positionData?.bufferedPosition ?? Duration.zero,
onChangeEnd: player.seek,
),
),
Text(
RegExp(r'((^0*[1-9]d*:)?d{2}:d{2}).d+$')
.firstMatch("$remaining")
?.group(1) ??
'$remaining',
style: Theme.of(context)
.textTheme
.caption
?.copyWith(color: Colors.white),
),
],
);
},
),
const SizedBox(height: 16),
Row(
children: [
IconButton(
onPressed: () {
switch (isVolumeDisabled) {
case true:
return _activateVolume();
default:
return _disableVolume();
}
},
icon: Icon(_getVolumeIcon(), size: 30, color: Colors.white),
),
Expanded(
child: Slider(
value: player.volume,
max: 1,
min: 0,
onChanged: (value) async {
setState(() {
volume = value;
});
await player.setVolume(value);
},
),
),
],
),
Center(
child: DropdownButton<String>(
dropdownColor: Colors.grey,
value: currentFileExtension, // 현재 선택된 값
items: const [
DropdownMenuItem(
value: 'mp3',
child: Text('mp3', style: TextStyle(color: Colors.white)),
),
DropdownMenuItem(
value: 'wav',
child: Text('wav', style: TextStyle(color: Colors.white)),
),
DropdownMenuItem(
value: 'opus',
child: Text('opus', style: TextStyle(color: Colors.white)),
),
DropdownMenuItem(
value: 'ogg',
child: Text('ogg', style: TextStyle(color: Colors.white)),
),
DropdownMenuItem(
value: 'flac',
child: Text('flac', style: TextStyle(color: Colors.white)),
),
],
onChanged: (String? newValue) {
setState(() {
currentFileExtension = newValue!;
_initialize(currentFileExtension);
});
},
),
),
],
),
);
}
}
출처 : https://kimhyeongi.tistory.com/64
[Flutter] Just_audio 패키지를 사용해서 음악을 삽입해보자
just_audio 앱을 개발하다보면 음악 플레이어를 삽입해야 하는 경우도 생길 때가 있습니다. 이러한 음악 플레이어를 외부의 패키지나 API 도움 없이 스스로 개발하려면 많은 어려움과 노력이 요구
kimhyeongi.tistory.com
LIST
'Flutter' 카테고리의 다른 글
| [Flutter] 다양한 setState() (0) | 2024.07.19 |
|---|---|
| [Flutter] Firebase 연동 (0) | 2024.07.06 |
| [Flutter] 다양한 방식의 DropdownButton (1) | 2024.06.19 |
| [Flutter] 다국어 지원 (0) | 2024.06.09 |