[Flutter] 일할 때 능률을 "더" 올리는 법

LOCKED·2023년 10월 14일
24
post-thumbnail

pub package

나는 일을 할 때 음악을 듣는 것을 선호한다. 능률이 오르는 편이기에..

여김없이 능률을 높인 채로 일을 하고 있다가 추천 재생 곡(youtube music)이 마음에 들지 않아 다음 곡으로 변경하려고 핸드폰에서 다음 곡 재생 버튼을 누르다가..

문득 이런 생각이 들었다.

'다음 곡으로 좀 더 편하게 넘길 수 있는 방법이 없을까?'

핸드폰에서 현재 재생 중인 곡이 마음에 들지 않아 다음 곡으로 넘기기 위해 핸드폰과 컴퓨터를 왔다갔다하는게 여간 번거로운 일이 아닐 수 없었다.

컴퓨터로 일에 집중하고 있다가 핸드폰을 조작한다는 것은 업무의 흐름을 끊기도 하고, 순간적인 집중력을 떨어트려 일의 능률을 저하시킬 수 있는 일이다.
(업무시간에 핸드폰을 자주 보는 좀 그렇기도..)

아무튼, 일의 능률은 올라가야 하기에...

컴퓨터에서 핸드폰의 재생 중인 음악을 다음 곡으로 넘기는 [Web/App]을 만들기로 마음 먹었다.

개발 기획

개발에 앞서 먼저 기획을 해본다면,

크게 Flutter Web, Flutter App, Fcm(Firebase cloud messging)를 이용할 것이다.

  1. App 에서 특정 topic으로 fcm의 메시지를 구독한다.
  2. Web 에서 다음 곡 버튼을 누르면 특정 topic으로 명령어(다음곡 재생) 포함된 slient message*를 보낸다.
  3. App 에서 silent message의 명령어를 읽어 해당 행동을 실행한다.

silent message는 fcm에서 notification영역을 제외한 data만을 보낸다.

  • 사용자에게 알림이 보이지 않는 특징이 있음

아주 간단하게 Flutter로 Web과 App을 개발하여 FCM으로 두 플랫폼을 연결할 것이다.

개발 진행

기획 단계에서 가장 중요한 점은
앱에서 현재 재생 중인 음악을 다음 곡으로 넘길 수 있는지 여부다.

Flutter 패키지들 중에 시스템에서 재생중인 미디어(음악)를 컨트롤할 수 있는 패키지를 찾아보았으나, 발견하지 못했다.

그래서 Android 기기에서 미디어를 컨트롤 할 수 있는 코드를 찾아 Flutter랑 연결하기로 하였다.

미디어 컨트롤 (kotlin)

private lateinit var audioManager: AudioManager;

// KeyEvent.KEYCODE_MEDIA_PLAY
// KeyEvent.KEYCODE_MEDIA_PAUSE
// KeyEvent.KEYCODE_MEDIA_NEXT
// KeyEvent.KEYCODE_MEDIA_PREVIOUS
private fun sendMediaButton(keyCode: Int) {
  var keyEvent = KeyEvent(KeyEvent.ACTION_DOWN, keyCode)
  audioManager!!.dispatchMediaKeyEvent(keyEvent)

  keyEvent = KeyEvent(KeyEvent.ACTION_UP, keyCode)
  audioManager!!.dispatchMediaKeyEvent(keyEvent)
}

미디어 버튼의 keyEvent를 이용하여 재생, 일시정지의 기능을 동작하는 코드다.

methodChannel(네이티브 통신)

flutter의 methodChannel을 이용하여 위 코드를 사용할 것이다.

// android side
// file: MainActivity.kt
methodChannel!!.setMethodCallHandler { call, result ->
    if (call.method == "skipNext") {
        sendMediaButton(KeyEvent.KEYCODE_MEDIA_NEXT)
        result.success(true);
    }
}
// flutter side
const MethodChannel channel = MethodChannel("lockedfile/system_media_control");
channel.invokeMethod('skipNext');

silent notification

FCM은 이제 더이상 legacy버전을 지원하지 않기 떄문에, httpv1을 사용한다.

참고 링크 https://firebase.google.com/docs/cloud-messaging/migrate-v1?hl=ko

헤더에 Authorization 토큰을 넣기 위해 googleapis_auth 패키지를 사용한다.

참고 링크 https://stackoverflow.com/questions/53862957/any-flutter-package-that-fetchs-googlecredential-access-token

silent notification은 다음과 같이 보내면 된다.

// post 
'message': {
    'data': {
        'action': 'skipNext' 
    }
}

이렇게 보내면 스마트폰의 알림은 울리지 않고 원하는 데이터만 앱에 전송할 수 있다.

이제 FirebaseMessaging.onBackgroundMessage를 통해 수신받은 메시지를 파싱하여 백그라운드에서 원하는 액션을 수행하면 된다.(앱이 켜있지 않더라도!)

FirebaseMessaging.onBackgroundMessage((message){
    final String? action = message.data['action'];
    if(action == 'skipNext'){
        // TODO: 여기에 다음 곡 이동 코드 추가
    }
});

그런데 테스트하다보니..

FirebaseMessaging.onBackgroundMessage내부에서 methodChannel을 사용할 수 없다고 한다..

그래서 급하게 관련 플러그인을 배포하였다.

system_media_controller [패키지/플러그인] 바로가기

화면 개발

위의 플러그인을 사용하여..
간단히 미디어를 제어할 수 있는 4개의 버튼으로만 화면을 구성하였다.

final _systemMediaControllerPlugin = SystemMediaController();

void excuteAction(MediaAction action) {
  if (kIsWeb) return;
  switch (action) {
    case MediaAction.play:
      _systemMediaController.play();
      break;
    case MediaAction.pause:
      _systemMediaController.pause();
      break;
    case MediaAction.skipNext:
      _systemMediaController.skipNext();
      break;
    case MediaAction.skipPrevious:
      _systemMediaController.skipPrevious();
      break;
  }
}

_actionCommand(MediaAction action) {
  excuteAction(action);

  Messaging.instance.sendData(
    data: {
      'action': action.name,
    },
  );
}

_skipNext() => _actionCommand(MediaAction.skipNext);
_skipPrevious() => _actionCommand(MediaAction.skipPrevious);
_play() => _actionCommand(MediaAction.play);
_pause() => _actionCommand(MediaAction.pause);

Widget build(BuildContext context) {
  return MaterialApp(
    home: Scaffold(
      appBar: AppBar(
        title: const Text('System Media Controller Example App'),
      ),
      body: Center(
        child: ButtonBar(
          alignment: MainAxisAlignment.center,
          children: [
            IconButton(
              onPressed: _play,
              icon: const Icon(Icons.play_arrow),
            ),
            IconButton(
              onPressed: _pause,
              icon: const Icon(Icons.pause),
            ),
            IconButton(
              onPressed: _skipNext,
              icon: const Icon(Icons.skip_next),
            ),
            IconButton(
              onPressed: _skipPrevious,
              icon: const Icon(Icons.skip_previous),
            ),
          ],
        ),
      ),
    ),
  );
}

이제 Web은 firebase hosting으로 배포하고, App은 apk로 빌드하여 핸드폰에 설치하면 끝이다.

Web에서는 Platform 작업을 안해둬서, Web일 떈 fcm을 보내는 로직만 동작하게 한다.

사용예시

왼쪽 하단에 있는 게 이번에 개발한 웹의 화면이다.

mac에서 pipper(화면 최상단에 브라우저를 유지하는 앱)에서 Web을 접속해 실사용 중이다.

확실히 다음곡으로 넘어갈 때, 핸드폰을 보지 않게 되서 일의 능률이 올라간 것을 느낀다..!!

테스트를 위해 silent notification이 아닌 일반 알림을 보냈고, 알림이 수신되면 시스템 미디어가 제어되는 것을 확인 할 수 있다.

  • 좌측은 Pipper를 사용하여 Web을 실행시켰다.
  • 우측은 scrcpy를 사용하여 Mobile을 미러링하였다.

TODO

재생/일시정지 버튼 합치기, 볼륨 조절, 현재 재생중인 곡의 정보 표시 등의 기능이 있으면 더 좋을 것 같은데...

일단 여기까지만...

음악은 컴퓨터 브라우저에서 듣는 게 좋을지도?...

profile
Flutter 개발자 :'>

11개의 댓글

comment-user-thumbnail
2023년 10월 16일

오ㅋㅋㅋ 재밌다ㅋㅋㅋㅋㅋ

1개의 답글
comment-user-thumbnail
2023년 10월 17일

아이디어 좋네요 ㅎㅎㅎ 실사용은 잘 하고 계신가요?

1개의 답글
comment-user-thumbnail
2023년 10월 20일

좋은 글 잘 봤습니다~
Push 로는 데이터 연동에 한계가 있을듯 보이는데,
Firebase store 를 이용해서 실시간 데이터를 연동하는 방법도 좋을것 같네요!

2개의 답글
comment-user-thumbnail
2023년 10월 26일

안녕하세요
궁금한 점이 있습니다
핸드폰 앱에서는 다운로드안 mp4 파일같은거를 실행 시켜 놓는건가요?

1개의 답글