+1

Customize MediaPlayer using TextureView

Như các bạn đã biết việc chạy một video, audio là rất phổ biến trong công việc coding nhất là với Android, với mỗi developer có vô vàn cách sử lý trong trường hợp này:

  • Code trực tiếp với MediaPlayer.
  • Gọi đến Intent (để cho thằng khác xử lý).
  • Hay sử dụng 1 open source, 1 lib đã được phát triển bên thứ 3.

Dù là cách nào đi chăng nữa trong nhân xử lý vẫn thông qua lớp MediaPlayer. Với yêu cầu đơn giản các developer thường gọi trực tiếp VideoView để sử lý (trong VideoView đã được cấu hình cơ bản các thuộc tính cần thiết cho play video, audio)

Các bài toán đơn giản ta không bàn đến ở đây, các trường hợp khác phải tuỳ biến vào yêu cầu người dùng, trường hợp cụ thể mà VideoView không đáp ứng được hoặc ta cũng có thể tìm hiểu sâu hơn VideoView làm thế nào.

Với cách sử dụng trực tiếp MediaPlayer để play video ta cần 1 holder để set cho display video (hay chính là hiển thị hình ảnh của video, audio đã được MediaPlayer xử lý rồi). Về cơ bản thông thường các developer hay sử dụng SurfaceView để xử lý việc setDisplay hay holder cho MediaPlayer, việc này rất cơ bản và phổ biến

--> Vậy tại sao ta cần bàn đến TextureView? Một số hạn chế của SurfaceView ta hay thường gặp

  • Việc rotate, scale holder thông qua surfaceView không dễ dàng chút nào
  • Việc render display (number frame/second) còn hạn chế
  • Customize view (hoặc add thêm view, draw object) trên holder của SurfaceView là điều không thể vì đã được sử dụng cho holder của MediaPlayer
  • Với sử dụng SurfaceView ta cần khai báo cố định về các thuộc tính width, height và position của nó, việc thay đổi vị trí, drag, drop là điều không thể
  • Ngoài ra cùng với tìm tòi cái mới trong việc code đã quá quen thuộc
  • Tuy với các hạn chế trên nhưng các bạn đừng coi thường về SurfaceView điển hình: Chrome sử dụng TextureView cho việc draw object trên bề mặt nhưng sau đó họ đã chuyển qua SurfaceView

Việc sử dụng TextureView cho MediaPlayer cũng vô cùng đơn giản. Trước hết ta cùng lược lại chút kiến thức về MediaPlayer

MEDIA PLAYER LIFECYCLE

mediaplayer_state_diagram.gif

Nhìn trên diagram thì ta thấy có đến 10 trạng thái khác nhau của MediaPlayer. Như vậy ta cũng phải xử lý View sao cho phù hợp (tránh trường hợp crash hay bug) về trạng thái của MediaPlayer. Từ diagram ta sẽ biết được sơ đồ hoạt động, và thứ tự hoạt động của MediaPlayer từ đó ta viết các callback hợp lý từ TextureView về parent

Ngoài ra với dữ liệu cuối chưa rõ ràng OnErorrListener ta cần xử lý từng trường hợp và đưa các thông báo cho người dùng biết chi tiết hay chính xác lỗi nằm ở đâu khi không play được video

Giả định ta phân tích lớp lỗi sau đây:

private static int getErrorMessage(final int frameworkError) {
    int messageId = R.string.play_error_message;

    if (frameworkError == MediaPlayer.MEDIA_ERROR_IO) {
        Log.e(TAG, "TextureVideoView error. File or network related operation errors.");
    } else if (frameworkError == MediaPlayer.MEDIA_ERROR_MALFORMED) {
        Log.e(TAG, "TextureVideoView error. Bitstream is not conforming to the related coding standard or file spec.");
    } else if (frameworkError == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
        Log.e(TAG, "TextureVideoView error. Media server died. In this case, the application must release the MediaPlayer object and instantiate a new one.");
    } else if (frameworkError == MediaPlayer.MEDIA_ERROR_TIMED_OUT) {
        Log.e(TAG, "TextureVideoView error. Some operation takes too long to complete, usually more than 3-5 seconds.");
    } else if (frameworkError == MediaPlayer.MEDIA_ERROR_UNKNOWN) {
        Log.e(TAG, "TextureVideoView error. Unspecified media player error.");
    } else if (frameworkError == MediaPlayer.MEDIA_ERROR_UNSUPPORTED) {
        Log.e(TAG, "TextureVideoView error. Bitstream is conforming to the related coding standard or file spec, but the media framework does not support the feature.");
    } else if (frameworkError == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
        Log.e(TAG, "TextureVideoView error. The video is streamed and its container is not valid for progressive playback i.e the video's index (e.g moov atom) is not at the start of the file.");
        messageId = R.string.play_progressive_error_message;
    }
    return messageId;
}

Ngoài ra các bạn có thể đưa ra các message phù hợp với các lỗi thường gặp trả về callback cho parent.

Bây giờ chúng ta hiểu làm như thế nào để MediaPlayer hoạt động, bước tiếp theo là quyết định cách chúng ta sẽ hiển thị video trên TextureView

CUSTOM CONTROLS

Tuỳ chính các chức năng cơ bản trong MediaPlayer như: Play, Pause, Progress, Previous, Next,... custom_controls copy.png

Thông qua lớp AnchorView

@Override
public  void  setAnchorView ( final  View  view )  {
    //set anchorView by customize FramePlayer with some component as: ImageView Play - Pause, ImageView Previous, ImageView Next, Seekbar Progress, TextView for title and something
}

Sau đây là làm sao để MediaPlayer hiển thị hình ảnh lên TextureView Ta chỉ cần sử dụng phương thức setSurface của MediaPlayer và dùng SurfaceTexture của TextureView

Một số thuộc tính và implements của MediaPlayer

mMediaPlayer . setOnPreparedListener ( mPreparedListener );
mMediaPlayer . setOnVideoSizeChangedListener ( mSizeChangedListener );
mMediaPlayer . setOnCompletionListener ( mCompletionListener );
mMediaPlayer . setOnErrorListener ( mErrorListener );
mMediaPlayer . setOnInfoListener ( mInfoListener );
mMediaPlayer . setOnBufferingUpdateListener ( mBufferingUpdateListener );

mMediaPlayer . setDataSource ( getContext (),  mUri ,  mHeaders );
mMediaPlayer . setSurface ( new  Surface ( mSurfaceTexture ));
mMediaPlayer . setAudioStreamType ( AudioManager . STREAM_MUSIC );
mMediaPlayer . setScreenOnWhilePlaying ( true );
mMediaPlayer . prepareAsync ();

Dưới đây là Full code customize 1 textview đã được add sẵn MediaPlayer với 2 thuộc tính: Video và Audio cho ai có nhu cầu sử dụng

Việc implements nó thực sự rất đơn giản, các bạn chỉ cần 1 FrameLayout để add nó, các thao tác trên nó rất đa dạng như drag, drop, touch, rotate

import android.content.Context;
import android.graphics.SurfaceTexture;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.view.Surface;
import android.view.TextureView;

import static android.content.Context.AUDIO_SERVICE;

public class VideoView extends TextureView implements MediaPlayer.OnErrorListener, MediaPlayer.OnPreparedListener,
        MediaPlayer.OnCompletionListener, MediaPlayer.OnVideoSizeChangedListener, MediaPlayer.OnBufferingUpdateListener,
        MediaPlayer.OnInfoListener,
        TextureView.SurfaceTextureListener {

    private MediaPlayer mediaPlayer;
    private MediaPlayer audioPlayer;
    private Uri videoPath;
    private Uri audioPath;
    private Surface surface;
    private VideoViewCallback videoViewCallback;
    private boolean prepared;
    private boolean needReset;
    private float bgVolume;
    private float movieVolume;
    private float maxVolume;
    private int positionPaused = -1;

    public VideoView(Context context) {
        super(context);
    }

    public VideoView(Context context, Uri videoPath) {
        super(context);

        final AudioManager audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
        maxVolume = audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);

        this.bgVolume = maxVolume;
        this.movieVolume = maxVolume;

        this.videoPath = videoPath;
        setSurfaceTextureListener(this);
        setOpaque(false);
        createMediaPlayer();
    }

    public void setVideoPath(Uri videoPath) {
        needReset = true;
        this.videoPath = videoPath;
    }

    public Uri getAudioPath() {
        return audioPath;
    }

    public float getBgVolume() {
        return bgVolume;
    }

    public float getMovieVolume() {
        return movieVolume;
    }

    public boolean isNeedReset() {
        return needReset;
    }

    private void createMediaPlayer() {
        mediaPlayer = new MediaPlayer();
    }

    public void release() {
        if (mediaPlayer != null) {
            mediaPlayer.setSurface(null);
            mediaPlayer.release();
            mediaPlayer = null;
            positionPaused = -1;
        }

        if (audioPlayer != null) {
            audioPlayer.release();
            audioPlayer = null;
        }
    }

    public void removeAudio() {
        if (audioPlayer != null) {
            audioPlayer.release();
            audioPlayer = null;
            positionPaused = -1;
        }

        this.audioPath = null;
    }

    public void setAudioPath(Uri audioPath) {
        this.audioPath = audioPath;

        if (audioPlayer == null) {
            audioPlayer = new MediaPlayer();
        }

        if (audioPath != null && StringUtils.valid(audioPath.toString())) {
            if (mediaPlayer != null) {
                mediaPlayer.reset();
            }

            try {
                audioPlayer.reset();
                audioPlayer.setDataSource(getContext(), audioPath);
                audioPlayer.setVolume(bgVolume / maxVolume, bgVolume / maxVolume);
                audioPlayer.prepareAsync();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    private void setAudioPath(Uri audioPath, boolean autoPlay) {
        setAudioPath(audioPath);

        if (autoPlay) {
            if (audioPlayer.getDuration() > positionPaused) {
                audioPlayer.seekTo(positionPaused);
            }

            audioPlayer.start();
        }
    }

    public void setVolume(float bgVolume, float movieVolume) {
        this.bgVolume = bgVolume;
        this.movieVolume = movieVolume;

        if (mediaPlayer != null) {
            mediaPlayer.setVolume(movieVolume / maxVolume, movieVolume / maxVolume);
        }

        if (audioPlayer != null) {
            audioPlayer.setVolume(bgVolume / maxVolume, bgVolume / maxVolume);
        }
    }

    public boolean isPlaying() {
        return mediaPlayer != null && mediaPlayer.isPlaying();
    }

    public void startPlayback() {
        if (mediaPlayer != null) {
            if (needReset) {
                mediaPlayer.seekTo(200);
            }
            mediaPlayer.start();
        }

        if (audioPlayer != null) {
            audioPlayer.start();
        } else if (audioPath != null && StringUtils.valid(audioPath.toString())) {
            setAudioPath(audioPath, true);
        }

    }

    public void pausePlayback() {
        if (mediaPlayer != null && mediaPlayer.isPlaying()) {
            positionPaused = mediaPlayer.getCurrentPosition();
            mediaPlayer.pause();
        }

        if (audioPlayer != null && audioPlayer.isPlaying())
            audioPlayer.pause();
    }

    public void resumePlayback() {
        if (mediaPlayer != null && mediaPlayer.isPlaying())
            mediaPlayer.start();

        if (audioPlayer != null && audioPlayer.isPlaying())
            audioPlayer.start();
    }

    public boolean isPrepared() {
        return prepared;
    }

    public void setVideoViewCallback(VideoViewCallback videoViewCallback) {
        this.videoViewCallback = videoViewCallback;
    }

    @Override
    public void onBufferingUpdate(MediaPlayer mp, int percent) {

    }

    @Override
    public void onCompletion(MediaPlayer mp) {
        positionPaused = -1;

        if (videoViewCallback != null) {
            videoViewCallback.onVideoComplete();
        }

        if (audioPlayer != null) {
            audioPlayer.seekTo(0);
        }
    }

    @Override
    public boolean onError(MediaPlayer mp, int what, int extra) {
        return false;
    }

    @Override
    public boolean onInfo(MediaPlayer mp, int what, int extra) {
        return false;
    }

    @Override
    public void onPrepared(MediaPlayer mp) {
        prepared = true;
        if (videoViewCallback != null) {
            videoViewCallback.onVideoPrepared();
        }
    }

    @Override
    public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {

    }

    @Override
    public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
        this.surface = new Surface(surface);
        loadMedia();
    }

    @Override
    public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {

    }

    @Override
    public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
        if (this.surface != null) {
            release();
            this.surface.release();
            this.surface = null;
        }

        return false;
    }

    @Override
    public void onSurfaceTextureUpdated(SurfaceTexture surface) {

    }

    public void loadMedia() {
        needReset = false;

        if (surface == null)
            return;

        if (mediaPlayer == null) {
            createMediaPlayer();
        }

        try {
            mediaPlayer.reset();
            mediaPlayer.setSurface(surface);
            mediaPlayer.setDataSource(getContext(), videoPath);
            mediaPlayer.setOnPreparedListener(this);
            mediaPlayer.setOnCompletionListener(this);
            mediaPlayer.setOnErrorListener(this);
            mediaPlayer.setOnVideoSizeChangedListener(this);
            mediaPlayer.setScreenOnWhilePlaying(true);
            mediaPlayer.setOnBufferingUpdateListener(this);
            mediaPlayer.setOnInfoListener(this);
            mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
            mediaPlayer.setVolume(movieVolume / maxVolume, movieVolume / maxVolume);
            mediaPlayer.prepareAsync();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public interface VideoViewCallback {
        void onVideoPrepared();

        void onVideoComplete();
    }

}

Ngoài ra các bạn có thể thảo luận thêm về việc nên sử dụng SurfaceView hay TextureView cho video của bạn Tại Google Groups


All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí