Media Playback trong Android (phần 1)

Hôm nay mình sẽ giới thiệu về Media Playback trong android. Một API khá hữu ích cho các bạn muốn tự làm dự án với các ứng dụng nghe nhạc.

Bạn có thể dễ dàng tích hợp audio, video và hình ảnh trong ứng dụng với sự hỗ trợ của Android multimedia framework. Bạn có thể chạy audio và video từ các file media lưu trữ trong resource của ứng dụng, từ các file độc lập trong filesystem, hoặc từ 1 dữ liệu stream qua kết nối mạng, tất cả sử dụng MediaPlayer APIs.

  1. Khai báo trong Manifest

    Trước khi bắt tay vào code, chúng ta cần khai báo các features liên quan của media player trong manifest.

    • Internet permission:
    <uses-permission android:name="android.permission.INTERNET" />
Wakelock permission: nếu ứng dụng của bạn cần giữ màn hình từ sleeping, hoặc sử dụng các hàm MediaPlayer.setScreenOnWhilePlaying(), MediaPlayer.setWakeMode(), bạn phải yêu cầu permission này:
    <uses-permission android:name="android.permission.WAKE_LOCK" />
  1. Sử dụng MediaPlayer

    Một trong những thành phần quan trọng nhất của media framework là class MediaPlayer. Một đối tượng của class này có thể fetch, mã hoá, play cả audio và video chỉ với tối thiểu cài đặt. Nó hỗ trợ một vài media resource khác nhau, ví dụ:

    • Local resource

    Internal URI, ví dụ bạn có thể nhận được từ một Content Resolver

    External URL (streaming)

    Đây là một ví dụ của cách play audio có trong local raw resource ( lưu trong thư mục res/raw của ứng dụng):

    MediaPlayer mediaPlayer = MediaPlayer.create(context, R.raw.sound_file_1);
	mediaPlayer.start(); // no need to call prepare(); create() does that for you
Trong trường hợp này, một raw resource là một file mà hệ thống không cố để parse bằng bất kỳ cách riêng nào. Mặc dù, nội dung của resource này không nên là raw audio. Nó nên là một file media đã được mã hoá và định dạng với 1 trong các định dạng được hỗ trợ.

Và đây là một cách bạn có thể play từ một URI tại địa phương ở trong hệ thống ( mà bạn có thể thu được qua một Content Resolver):
    Uri myUri = ....; // initialize Uri here
    MediaPlayer mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setDataSource(getApplicationContext(), myUri);
    mediaPlayer.prepare();
    mediaPlayer.start();
Playing từ một remote URL qua HTTP stream sẽ giống như thế này:
    String url = "http://........"; // your URL here
    MediaPlayer mediaPlayer = new MediaPlayer();
    mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
    mediaPlayer.setDataSource(url);
    mediaPlayer.prepare(); // might take long! (for buffering, etc)
    mediaPlayer.start();
Chú ý: bạn phải bắt hoặc pas cả 2 IllegalArgumentException và IOException khi sử dụng setDataSource(), vì file bạn  đang tham khảo có thể không tồn tại.
  1. Sự chuẩn bị không đồng bộ

    Sử dụng MediaPlayer có thể đơn giản về nguyên tắc. Mặc dù nó rất quan trọng để nhớ một vài thứ cần thiết để tích hợp nó tương ứng với một ứng dụng Android điển hình. Ví dụ, để gọi tới prepare() có thể tốn một thời gian dài để thực hiện, vì nó có thể liên quan đến fetching và decoding dữ liệu media. Vì thế, giống như trường hợp với bất kỳ một phương thúc tốn nhiều thời gian để thực hiện, bạn không bao giờ nên gọi nó từ UI thread của ứng dụng. Làm thế sẽ là nguyên nhân của việc UI phải chờ cho tới khi phương thức thực hiện xong, một trải nghiệm rất tệ của người dùng và có thể là nguyên nhân của ANR ( Application Not Responding) error. Kể cả bạn mong muốn resource load nhanh, nhớ rằng bất cứ thứ gì tốn thời gian hơn 10 giây để phản hồi trong UI sẽ là nguyên nhân của một sự pause đáng chú ý và sẽ gây cho người dùng ấn tượng rằng ứng dụng của bạn chậm.

    Để tránh sự trễ trong UI thread của bạn, tạo một thread khác để chuẩn bị cho MediaPlayer và chú ý main thread khi xong. Mặc dù, trong khi bạn có thể tự viết một threadinng logic, một mẫu chung rất thuận lợi khi sử dụng MediaPlayer với sự hỗ trợ của framework là phương thức prepareAsync(). Phương thức này bắt đầu để chuản bị media trong background và trả về ngay lập tức. Khi media chuẩn bị xong, hàm onPrepared() của MediaPlayer.OnPreparedListener, cài đặt qua setOnPreparedListener() sẽ được gọi.

  2. Quản lí trạng thái

    Một khía cạnh khác của MediaPlayer mà bạn cần nhớ là state-based. MediaPlayer có một internal state mà bạn luôn phải chú ý khi viết code, vì một số hoạt động chỉ có hiệu lực khi người chơi ở trong một trang thái đặc biệt. Nếu bạn thực hiện một tiến trình trong trạng thái sai, hệ thống có thể sẽ ném vào một exception hoặc gây ra một hành vi không đoán trước được.

    Ví dụ, khi tạo một MediaPlayer, nó ở trạng thấy Idle, bạn nên khởi tạo nó với setDataSource() khi ở trạng thái khởi tạo. Sau đó, bạn phải chuẩn bị cho nó với cả 2 hàm prepare() và prepareAsync(). Khi MediaPlayer đã chuẩn bị xong, nó sẽ vào trạng thái Prepared, tức là bạn có thể gọi start() để play media. Lúc này, bạn có thể chuyển giữa các trạng thái Started, Paused, và PlaybackCompleted bởi các hàm start(), pause(), seekTo() ở trong chúng. Khi gọi stop(), chú ý rằng bạn không thể gọi start() lần nữa cho đến khi chuẩn bị xong MediaPlayer.

  3. Giải phóng MediaPlayer

    Một MediaPlayer có thể tiêu thụ tài nguyên quý giá của hệ thống, vì thế khi bạn thực hiện xong với nó, cần luôn nhớ gọi hàm release() để đảm bảo bất kỳ tài nguyên hệ thống nào đã được phân phát sẽ được giải phóng.

    Đây là cách bạn nên giải phóng MediaPlayer:

    mediaPlayer.release();
	mediaPlayer = null;
Ví dụ, vấn đề có thể xảy ra nếu bạn quên giải phóng MediaPlayer khi activity stop, nhưng lại tạo 1 cái mới khi activity start lần nữa. Như bạn đã biết,  khi user thay đổi chiều của màn hình, hệ thống sẽ xử lí bằng cách khởi động lại activity (default), vì thế bạn có thể nhanh chóng sử dụng hết tài nguyên hệ thống khi user xoay thiết bị trở lại, bạn tạo mới 1 MediaPlayer mà bạn chưa bao giờ giải phóng.
  1. Sử dụng service với MediPlayer

    Nếu bạn muốn media play trong background ngay cả khi ứng dụng không ở trên màn hình, là khi bạn muốn media tiếp tục chạy khi bạn sử dụng 1 ứng dụng khác, bạn cần bắt đầu 1 Service và điều khiển MediaPlayer từ đó.

    6.1 Running asynchronously

     Trước hết, giống một Activity, tất cả công việc của service hoàn thành trên một single thread với mặc đinh, nếu bạn chạy một activity và một service trên cùng một ứng dụng, nó sẽ sử dụng cùng một thread ( main thread) với mặc định. Theo đó, các service cần thực hiện các intent đến một cách nhanh chóng và không bao giờ xử lí các tính toán dài khi phản hồi đến chúng. Nếu có bất kỳ công việc nặng nào, bạn phải làm các task đó không đồng bộ: cả ở thread khác bạn tự implement, hoặc sử dụng sự thuận tiện của framework để đồng bộ tiến trình.
    
     Ví dụ, khi sử dụng một MediaPlayer từ main thread, bạn nên gọi prepareAsync() hơn là prepare(), và implement MediaPlayer.OnPreparedListener.
    
        public class MyService extends Service implements MediaPlayer.OnPreparedListener {
            private static final String ACTION_PLAY = "com.example.action.PLAY";
            MediaPlayer mMediaPlayer = null;

            public int onStartCommand(Intent intent, int flags, int startId) {
                ...
                if (intent.getAction().equals(ACTION_PLAY)) {
                    mMediaPlayer = ... // initialize it here
                    mMediaPlayer.setOnPreparedListener(this);
                    mMediaPlayer.prepareAsync(); // prepare async to not block main thread
                }
            }

            /** Called when MediaPlayer is ready */
            public void onPrepared(MediaPlayer player) {
                player.start();
            }
        }
6.2 Xử lí lỗi không đồng bộ
	Trong quá trình đồng bộ, lỗi thông thường sẽ được báo hiệu với một ngoại lệ hoắc một mã lỗi. Bạn nên chắc chắn rằng ứng dụng của bạn được thông báo của các lỗi tương ứng. Trong trường hợp của MediaPlayer, bạn có thể thực hiện bằng cách implement MediaPlayer.OnErrorListener:
        public class MyService extends Service implements MediaPlayer.OnErrorListener {
            MediaPlayer mMediaPlayer;

            public void initMediaPlayer() {
                // ...initialize the MediaPlayer here...

                mMediaPlayer.setOnErrorListener(this);
            }

            @Override
            public boolean onError(MediaPlayer mp, int what, int extra) {
                // ... react appropriately ...
                // The MediaPlayer has moved to the Error state, must be reset!
            }
        }
  1. Sử dụng wakelock Khi thiết kế một ứng dụng chạy nhạc trên background, thiết bị có thể sleep trong khi service vẫn đang chạy. Vì hệ thống Android cố gắng tiết kiệm pin khi thiết bị sleeping, hệ thống sẽ cố để tắt bất kì feature nào không cần thiết, kể cả CPU và phần cứng WiFi. Mặc dù, nếu bạn đang chạy hoặc stream nhạc, bạn muốn tránh hệ thống gây trở ngại cho playback của bạn. Để chắc chắn rằng service của bạn tiếp tục chạy dưới những điều kiện này, bạn phải sử dụng wakelock. Một wakelock là một cách để báo hiệu với hệ thống rằng ứng dụng của bạn đang sử dụng một số feature nên có sẵn kể cả nếu điện thoại của bạn ở chế độ nghỉ. Để chắc chắn rằng CPU tiếp tục chạy trong khi MediPlayer đang chạy, gọi hàm setWakeMode() khi khởi tạo MediaPlayer.
    mMediaPlayer = new MediaPlayer();
    // ... other initialization here ...
    mMediaPlayer.setWakeMode(getApplicationContext(), PowerManager.PARTIAL_WAKE_LOCK);
Nếu bạn đang stream media qua network và bạn đang dùng WiFi, có lẽ bạn muốn giữ WifiLock, bạn phải yêu cầu và giải phóng. Vì thế, khi bạn bắt đầu chuẩn bị MediaPlayer với chế độ URL, bạn nên tạo và mua một Wifilock. Ví dụ:
    WifiLock wifiLock = ((WifiManager) getSystemService(Context.WIFI_SERVICE))
        .createWifiLock(WifiManager.WIFI_MODE_FULL, "mylock");

    wifiLock.acquire();
Khi bạn pause hay stop media, hoặc khi bạn không cần network nữa, bạn nên release nó:
    wifiLock.release();
  1. Tổng kết Đây là những giới thiệu ban đầu về MediaPlayer, mong rằng sẽ giúp ích cho các bạn.