TIL

TIL - MUOZ (2) (오디오 플레이어 UI)

hamiric 2025. 2. 8. 01:10

## 해당 TIL은 주어진 과제를 수행하면서 얻은 학습 내용과, 시행착오 등등을 종합해서 작성한것임

MUOZ 프로젝트를 진행하면서, 새롭게 생각해야 될게 많았고, 너무 정신이 없던 나머지 TIL 작성을 약간 미루었던거 같다..

TIL 작성할 만한 부분은 꽤 있었는데, 시간에 쫓겨서..

약 2~3주간 프로젝트를 진행하면서 있었던 TIL 부분들을 한번에 우다다다 쏟아내 보고자 한다.

 

GitHub - Oz-player/oz_player

Contribute to Oz-player/oz_player development by creating an account on GitHub.

github.com

 

오디오 플레이어 UI 를 만드는데 있어, 아래 유튜브를 많이 참고 하였다.

참고로, 2025.02 인 현재로써, 아래 유튜브에서 사용하고 있는 MusicMatch 의 가사 기능은 유료컨텐츠로 넘어간듯 보인다.

< 참고 자료 >

 

 

 

1.  앱 전체에서 사용되는 오디오 플레이어

우선 이번 프로젝트에서 사용되는 오디오 플레이어는, 일단 앱 전체에서 지속적으로 음악을 플레이 시켜주어야 한다.

즉, 앱 전체에서 Audio Player 객체가 살아 있어야 한다는 의미이기 때문에, Provider를 통해 Main.dart의 build에서 부터 audioPlayer가 살아 있도록 하였다.

// 오디오 플레이어 Provider
final audioPlayerViewModelProvider =
    AutoDisposeNotifierProvider<AudioPlayerViewModel, AudioPlayerState>(() {
  return AudioPlayerViewModel();
});


// main.dart에서 build 때 생성되도록 watch를 걸어준 모습
// Riverpod 의 ConsumerWidget을 이용
class MyApp extends ConsumerWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context, WidgetRef ref) {
    ref.watch(audioPlayerViewModelProvider);

    return MaterialApp.router(
      title: 'Oz Player',
      themeMode: ThemeMode.light,
      theme: lightTheme,
      darkTheme: darkTheme,
      debugShowCheckedModeBanner: false,
      routerConfig: router,
    );
  }
}

 

또한, 오디오 플레이어는 앱 내에서 ModalBottomSheet 로 제공되므로,

AudioBottomSheet 라는 클래스를 만들고 modalSheet를 전역적으로 사용할 수 있게끔 만들었다.

class AudioBottomSheet {
  static void show(BuildContext context, int index) {
    showModalBottomSheet(
      context: context,
      isDismissible: false,
      enableDrag: true,
      isScrollControlled: true,
      builder: (context) {
        return Consumer(
          // 오디오 플레이어 UI
        );
      }
    )
  }
}

 

다른 페이지에서 해당 오디오 플레이어(BottomModalSheet)를 사용할때는 다음과 같이 사용하면 된다.

// context는 BuildContext
// positionIndex는 현재 음악카드의 위치정보 --> MUOZ 앱내 기능
AudioBottomSheet.show(context, positionIndex);

 

 

2.  오디오 플레이어 UI

오디오 플레이어 UI는 다음과 같다.

 

여기서 밑의 ProgressBar와 음악을 연동시킨 방법을 소개하고자 한다.

 

직전 포스팅인 Just Audio에서 음악을 설정하게 되면,

Audio Player는 현재 재생 정보를 가지고 있는 것에 대해서 다양한 정보를 제공해 주는데,

ProgressBar에 가장 중요한 역할을 수행하는 positionStream 정보 또한 제공해 준다.

따라서, StreamBuilder 를 이용하여, 해당 위젯의 값을 Stream으로 넘겨주면,

오디오가 진행됨에 따라 ProgressBar가 같이 진행되는 것을 확인할 수 있다.

StreamBuilder(
  stream: audioState.audioPlayer.positionStream,
  builder: (context, snapshot) {
    Duration total = audioState.audioPlayer.duration ?? const Duration(seconds: 0);

    return ProgressBar(
      progress: snapshot.data ?? const Duration(seconds: 0),
      total: total,
      buffered: audioState.audioPlayer.bufferedPosition,
      timeLabelTextStyle: TextStyle(color: Color(0xff7303E3)),
      timeLabelPadding: 10,
      baseBarColor: Color(0xffF2E6FF),
      progressBarColor: Color(0xff7303E3),
      bufferedBarColor: Color(0xffD9B3FE),
      thumbColor: Color(0xff7303E3),
      onSeek: (duration) {
        ref.read(audioPlayerViewModelProvider.notifier).skipForwardPosition(duration);
      },
    );
  },
});

 

중요한 기능만 좀 소개하자면,

total 값은 Audio Player에 세팅된 오디오의 전체 길이를 의미하고,

 

buffered 값은 현재 버퍼링된 오디오 부분을 나타내 준다.

(해당 기능은 외부 Url을 통해 오디오 플레이어를 재생할 경우 유용한 옵션이다.)

 

onSeek 같은 경우, ProgressBar를 직접 조작했을 경우 호출되는 로직이다.

 

 

2.  소형 오디오 플레이어 UI

저런 화면 전체에 나오는 오디오 플레이어 말고도,

음악이 재생되는 와중 다른 페이지로 이동했을때에도, 음악이 재생되고 있다는 표시를 해 주는 소형 오디오 플레이어가 필요했다.

 

이와 같은 느낌으로, Audio Player에 연결된 오디오가 존재할 경우, 소형 플레이어가 존재해야 하며,

소형 플레이어의 특징으로는 좌우로 스와이프를 할 경우, 완전히 플레이어를 종료 시킬 수 있어야 했다.

 

이에 Dismissible 이란 위젯을 적극적으로 활용하여 해당 기능을 만들었는데, 코드는 다음과 같다.

  Widget audioBox(
      AudioPlayerState audioState, WidgetRef ref, BuildContext context) {
    return Dismissible(
      key: Key('audio_player'),
      direction: DismissDirection.horizontal,
      onDismissed: (direction) {
        ref.read(audioPlayerViewModelProvider.notifier).toggleStop();
      },
      child: GestureDetector(
        onTap: () {
          AudioBottomSheet.showCurrentAudio(context);
        },
        child: Padding(
          // 소형 오디오 플레이어 내부 위젯
        )
      )
    );
  }
)

 

아, 소형 플레이어의 또 하나의 특징이라면

뒷 배경이 블러처리되어 약간 반투명하게 보이도록 만들어야 했다는 것이다.

아래와 같이 ImageFilter.blur를 이용하여, 반투명한 블러 위젯을 만들 수 있었다.

Padding(
  padding: const EdgeInsets.symmetric(horizontal: 20),
  child: Stack(
    children: [
      Positioned(
        top: 0,
        bottom: 0,
        left: 0,
        right: 0,
        child: ClipRRect(
          borderRadius: BorderRadius.circular(12),
          child: BackdropFilter(
            filter: ImageFilter.blur(sigmaX: 10, sigmaY: 10),
            child: Container(
              width: double.infinity,
              height: 60,
              color: colorMode
                  ? Colors.black.withValues(alpha: 0.3)
                  : Colors.white.withValues(alpha: 0.3),
            ),
          ),
        ),
      ),
      Container(
        decoration: BoxDecoration(
          borderRadius: BorderRadius.circular(12),
        ),
        child: Padding(
          // 소형 플레이어 내부 위젯
        ),
      ),
    ],
  ),
);