Flutter로 만드는 오디오 플레이어

Flutter는 오픈 라이브러리의 문서가 생각보다 잘 안 되어있는 경우가 많다. Star나 점수가 꽤 높은데도 문서화에 누락되어 있는 부분도 많고, 최신화가 잘 안되어 있다.

그래서 찾기 힘들었던 내용들을 정리해 본다.

1. audio_service에서 백그라운드 컨트롤 등록

백그라운드 컨트롤러에 동작을 등록하거나 상태를 만들 때 playbackState에 상태를 등록하는 데 systemActions로 실제 수행될 수 있는 동작을 등록해야 백그라운드 컨트롤러에서의 동작들도 정상적으로 동작한다.

playbackState.add(playbackState.value.copyWith(
  controls: [MediaControl.play],
  processingState: AudioProcessingState.loading,
  systemActions: {
    MediaAction.seek,
    MediaAction.play,
    MediaAction.playPause,
    MediaAction.skipToNext,
    MediaAction.skipToPrevious,
  },
));

2. OneTime URL 사용으로 인한 Queue 사용이 어려운 문제

URL이 고정인 경우, 모든 URL을 미리 받아놓고 이를 플레이리스트로 관리하면서 빠르게 전환하고 쉽게 Next, Prev를 구현할 수 있지만, OneTime URL을 사용하고 있어서 이게 불가능했다.

따라서 전체 코드를 공유할 수 없지만, 일부만 얘기해 보자면 플레이리스트를 글로벌 BLoC 객체로 관리하고, 이 데이터를 기준으로 skipToNext 함수를 구현한다.

@override
Future<void> skipToNext() async {
  if (playlist != null) {
    if (playlist!.current < playlist!.clips.length - 1) {
      playlist!.current++;
      final entry = playlist!.clips[playlist!.current];
      mediaItem.add(MediaItem(
        id: entry["id"].toString(),
        title: entry["title"],
        artUri: Uri.parse(entry["thumbnail"]),
        duration: Duration(seconds: entry["length"]),
      ));
    }
    playbackState.add(playbackState.value.copyWith(
      updatePosition: Duration.zero,
    ));
    return playClip(playlist!.current);
  }
}

코드를 보면 알 수 있듯이, BLoC에 등록된 플레이리스트를 확인한 후에 다음 곡의 mediaItem을 등록하고 백그라운드 상태를 갱신하고, 실제 재생하는 코드를 호출한다.

기존 문서의 예제를 보면 skipToNext가 이미 구현되어 있어 오버라이딩만 하면 되는데, 그렇게 할 수 없었다. 직접 플레이리스트를 보고 확인한 후에 OneTime URL을 받아서 직접 재생할 수밖에 없었다.

한 가지 더 팁을 공유하자면, 처음 설계할 때부터 플레이리스트는 BLoC으로 관리하는 것을 권장한다. 왜냐하면 플레이어 앱은 생각 이상으로 많은 화면에서 재생을 제어해야 하고 백그라운드로도 제어해야 한다. 따라서 BLoC 객체로 언제나 띄워져 있어야 하고 모든 처리가 BLoC의 이벤트를 받아서 리스너에서 처리돼야, 구현이 쉽다.

내 경우 처음 설계 시 BLoC을 사용하지 않았더니, 중간에 꽤 고생해서 수정했다.

3. 구간 반복의 구현

이미 오디오 플레이어를 만들고 있다면 그리 어렵지 않게 구현할 수 있겠지만, 짧게 팁을 공유하자면, positionStream으로 구현할 수 있다.

player.positionStream.listen((event) {
  if (startPos != -1 && endPos != -1) {
    if (event.inSeconds < startPos || event.inSeconds > endPos) {
      player.seek(Duration(seconds: startPos));
    }
  }
});

positionStream의 경우 이벤트가 Duration 객체로 전달되는데, AudioHandler에 구간 반복하고자 하는 구간의 값을 저장해 두고, 이 값에 따라서 seek을 통해 구현할 수 있다.

오픈해도 앱을 밝히긴 어렵지만, 언젠가 LAH에서도 비슷한 앱을 만들 수도 있지 않을까.

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다