android MediaPlayer正确使用姿势

android MediaPlayer正确使用姿势!!!

mediaplayer在使用时候还好需要注意很多细节,现在很多播放都是推荐使用exoplayer ,但是毕竟原生的使用掌握还是很有必要的,为什么mediaplayer使用时候会有很多不顺手地方:

MediaPlayer 在不同状态下使用某些 API 会出现异常,主要有以下原因:

一、MediaPlayer 的状态机模型

MediaPlayer 遵循特定的状态机模型,它有多个状态,如 Idle(空闲)、Initialized(初始化)、Prepared(准备好)、Started(开始播放)、Paused(暂停)、Stopped(停止)、Ended(播放结束)等。在不同的状态下,只有特定的 API 调用是合法的,否则会引发异常。

例如,在 Idle 状态下,直接调用 start() 方法会抛出异常,因为此时 MediaPlayer 还没有被初始化或设置媒体源。

同样,在未经过正确的状态转换就调用某些方法也会导致问题。比如,在没有调用 prepare() 或 prepareAsync() 使 MediaPlayer 进入 Prepared 状态之前,调用 start() 也是不允许的。

二、资源管理和同步问题

MediaPlayer 在不同状态下对资源的管理和同步有严格的要求。

当处于特定状态时,某些 API 可能会触发对资源的访问或操作,如果此时资源未正确初始化或处于不一致的状态,就会引发异常。

例如,在播放过程中(Started 状态),如果尝试设置新的媒体源而没有先停止当前播放并正确处理状态转换,可能会导致混乱和异常。

三、内部逻辑和线程安全考虑

MediaPlayer 内部可能涉及多个线程的协作,如播放线程、解码线程等。

在不同状态下,这些线程的行为和交互是有特定规则的。如果不按照正确的状态顺序调用 API,可能会破坏这些规则,导致线程之间的同步问题和异常。

例如,在某些状态下,某些 API 可能会触发异步操作,如果在不适当的时候调用这些 API,可能会导致与其他正在进行的操作冲突,引发异常。

总之,为了避免在使用 MediaPlayer 时出现异常,必须严格按照其状态机模型正确地进行状态转换和 API 调用,以确保资源的正确管理和线程的安全协作。

下面编写两个工具类可以解决不顺手问题:

java 复制代码
package com.github.jasonhancn.tvcursor.util;

import android.content.Context;
import android.media.MediaPlayer;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.SurfaceHolder;

import com.realtop.mqttutils.MUtils;

public class NewPlayer {

    private static final String TAG = "NativePlayer";
    private final Handler handler;
    private final Handler.Callback callback;
    private final String path;
    private final SurfaceHolder mSurfaceHolder;
    private final Context context;
    private MediaPlayer mediaPlayer;
    private boolean isCloseByUser;
    private long start;
    public static final int end_state = 1;
    public static final int error_state = 3;
    public static final int prepare_state = 5;
    public static final int progress_state = 6;
    private boolean isPrepared;//全部进入状态 可以关闭释放了
    private boolean isEnterComplete = false;//防止完成多次调用
    private boolean isEnterPrepare = false;//防止准备完成多次调用
    private boolean isEnterError = false;//防止错误多次调用
    boolean isWorking = false;
    long seekTime = -1;
    private int isPause = -1;
    int videoWidth = -1;
    int videoHeight = -1;
    long videoTotalTime = 0;

    public void setSeekTime(long seekTime) {
        this.seekTime = seekTime;
        syncSeek();
    }

    public NewPlayer(Context context, String path, SurfaceHolder mSurfaceHolder, Handler.Callback callback) {
        this.callback = callback;
        this.path = path;
        this.mSurfaceHolder = mSurfaceHolder;
        this.context = context;
        this.handler = new Handler(Looper.getMainLooper());
        initPlayer();
    }


    public void initPlayer() {
        start = System.currentTimeMillis();
        try {
            mediaPlayer = new MediaPlayer();
            mediaPlayer.setDataSource(path);
            Log.i(TAG, "Init: player video path:" + path);

            if (mSurfaceHolder != null) mediaPlayer.setDisplay(mSurfaceHolder);

            mediaPlayer.setOnPreparedListener(iMediaPlayer -> handler.post(this::onPrepareCallback));
            mediaPlayer.setOnCompletionListener(iMediaPlayer -> handler.post(this::onCompleteCallback));
            mediaPlayer.setOnErrorListener((iMediaPlayer, i, code) -> {
                handler.post(() -> onErrorCallback(i, "error code:" + code));
                return true;
            });

            mediaPlayer.prepareAsync();
        } catch (Exception e) {
            final String errorMsg = e.getMessage();
            handler.post(() -> onErrorCallback(-118, errorMsg));
        }

    }

    public long getCurrentTime() {
        if (mediaPlayer != null && isWorking) {
            return mediaPlayer.getCurrentPosition();
        }
        return 0;
    }

    public long getTotalTime() {
        return videoTotalTime;
    }

    public int getWidth() {
        return videoWidth;
    }

    public int getHeight() {
        return videoHeight;
    }

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


    void syncSeek() {
        if (seekTime != -1 && isWorking && mediaPlayer != null) {
            seekTime = Math.min(seekTime, getTotalTime());
            seekTime = Math.max(0, seekTime);
            mediaPlayer.seekTo((int) seekTime);
            seekTime = -1;
        }
    }

    public void setPause(boolean pause) {
        isPause = pause ? 1 : 0;
        syncPause();
    }

    void syncPause() {
        if (isWorking && mediaPlayer != null && isPause != -1) {
            if (isPause == 1 && mediaPlayer.isPlaying()) {
                mediaPlayer.pause();
            } else if (isPause == 0 && !mediaPlayer.isPlaying()) {
                mediaPlayer.start();
            }
            isPause = -1;
        }
    }

    private void onPrepareCallback() {
        isPrepared = true;
        isWorking = true;

        //获取视频信息
        videoWidth = mediaPlayer.getVideoWidth();
        videoHeight = mediaPlayer.getVideoHeight();
        videoTotalTime = mediaPlayer.getDuration();

        // 准备好前已经人工停止了
        if (isCloseByUser) {
            Log.i(TAG, "onPrepareCallback: user close media play:" + mediaPlayer);
            releasePlayer();
            return;
        }

        if (isEnterPrepare) {
            Log.i(TAG, "onPrepareCallback: again enter prepare");
            return;
        }
        isEnterPrepare = true;

        syncSeek();

        if (isPause == -1) { // 如果用户没有暂停操作的话执行
            mediaPlayer.start();
        }

        syncPause();

        handler.post(() -> {
            Message msg = Message.obtain();
            msg.what = prepare_state;
            callback.handleMessage(msg);
        });
        Log.i(TAG, "onPrepareCallback: prepare cost time:" + (System.currentTimeMillis() - start) + "; is playing:" + mediaPlayer.isPlaying());
    }

    private void onCompleteCallback() {
        isPrepared = true;
        isWorking = false;

        if (isEnterComplete) {
            Log.i(TAG, "onCompleteCallback: again enter");
            return;
        }
        isEnterComplete = true;

        releasePlayer();

        handler.post(() -> {
            Message msg = Message.obtain();
            msg.what = end_state;
            msg.obj = "end";
            callback.handleMessage(msg);
        });
        Log.i(TAG, "completeCallback: video player end:" + (System.currentTimeMillis() - start) / 1000);
    }

    private void onErrorCallback(int i, String str) {
        isPrepared = true;
        isWorking = false;

        if (isEnterError) {
            Log.i(TAG, "onErrorCallback: again enter");
            return;
        }
        isEnterError = true;

        releasePlayer();

        handler.post(() -> {
            Message msg = Message.obtain();
            msg.what = error_state;
            msg.obj = "err:" + str;
            callback.handleMessage(msg);
        });
        Log.i(TAG, "errorCallback: play error code:" + i + "; use time:" + (System.currentTimeMillis() - start) / 1000);
        MUtils.showMsg(context, "Video play failure ! error code:" + i + "; msg:" + str);
    }

    // 主线程
    public void stopPlayerRelease() {
        isCloseByUser = true;
        releasePlayer();
    }

    public boolean isEnd() {
        return mediaPlayer == null;
    }

    private void releasePlayer() {
        handler.removeCallbacksAndMessages(null);

        Log.i(TAG, "Release: enter: isPrepared:" + isPrepared
                + "; isCloseByUser:" + isCloseByUser + "; media player:" + mediaPlayer);

        if (!isPrepared || mediaPlayer == null) return;

        try {
            if (mediaPlayer.isPlaying()) {
                mediaPlayer.stop();
                Log.i(TAG, "Release: enter stop player");
            }
        } catch (Exception e) {
            Log.i(TAG, "Release: pause error:" + e.getMessage());
        }

        try {
            mediaPlayer.setOnCompletionListener(null);
            mediaPlayer.setOnPreparedListener(null);
            mediaPlayer.setOnErrorListener(null);
        } catch (Exception e) {
            Log.i(TAG, "Release: close listener error:" + e.getMessage());
        }

        try {
            mediaPlayer.setDisplay(null);
        } catch (Exception e) {
            Log.i(TAG, "releasePlayer: set display null error:" + e.getMessage());
        }

        try {
            mediaPlayer.release();
        } catch (Exception e) {
            Log.i(TAG, "Release: release error:" + e.getMessage());
        }

        mediaPlayer = null;
        Log.i(TAG, "Release: handle ok");
    }

    public static int[] calculateImageSize(Context context, int imageWidth, int imageHeight) {
        DisplayMetrics displayMetrics = context.getResources().getDisplayMetrics();
        int screenWidth = displayMetrics.widthPixels;
        int screenHeight = displayMetrics.heightPixels;

        int newWidth = imageWidth;
        int newHeight = imageHeight;

        if (imageWidth > screenWidth || imageHeight > screenHeight) {
            float ratioWidth = (float) imageWidth / screenWidth;
            float ratioHeight = (float) imageHeight / screenHeight;
            float ratio = Math.max(ratioWidth, ratioHeight);

            newWidth = (int) (imageWidth / ratio);
            newHeight = (int) (imageHeight / ratio);
        }

        return new int[]{newWidth, newHeight};
    }


}

还有一个类似videoview的工具类

java 复制代码
package com.github.jasonhancn.tvcursor.util;

import android.content.Context;
import android.os.Handler;
import android.os.Looper;
import android.os.Message;
import android.text.TextUtils;
import android.util.AttributeSet;
import android.util.Log;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;
import android.view.ViewParent;

import androidx.annotation.NonNull;

public class NewPlayerView extends SurfaceView implements SurfaceHolder.Callback2, Handler.Callback {
    private static final String TAG = "JasonVideoView";
    private SurfaceHolder mHolder;
    NewPlayer player;
    String videoPath;
    String lastVideoPath;
    long currentSeekTime;
    private Context mContext;
    Handler handler = new Handler(Looper.getMainLooper());
    Handler.Callback callback;
    private long userSeekTime = -1;
    private int userPause = -1;
    int videoWidth = -1;
    int videoHeight = -1;
    long videoTotalTime = 0;

    public void setCallback(Handler.Callback callback) {
        this.callback = callback;
    }

    public void setVideoPath(String videoPath) {
        this.videoPath = videoPath;
        this.currentSeekTime = 0;
        this.userPause = -1;
        this.userSeekTime = -1;

        this.lastVideoPath = videoPath;
        resetVideoInfo();

        releasePlayer();
        initPlayer();
    }

    public NewPlayerView(Context context) {
        super(context);
        init(context);
    }

    public NewPlayerView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context);
    }

    public NewPlayerView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context);
    }

    public NewPlayerView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);
        init(context);
    }

    private void initPlayer() {
        if (player != null || mHolder == null || TextUtils.isEmpty(videoPath)) return;
        player = new NewPlayer(mContext, videoPath, mHolder, this);

        if (userSeekTime == -1) {
            player.setSeekTime(currentSeekTime);
        }

        syncSeek();

        syncPause();

        handler.post(syncTimeStateRunnable);
        Log.i(TAG, "initPlayer: enter:" + player);
    }

    public void goForward() {
        if (getTotalTime() > 0) {
            long go = (long) (getCurrentTime() + getTotalTime() * 0.1f);
            setSeek(go);
        }
    }

    public void goBack() {
        if (getTotalTime() > 0) {
            long go = (long) (getCurrentTime() - getTotalTime() * 0.1f);
            setSeek(go);
        }
    }

    public void setSeek(long time) {
        if (isEnd() && !TextUtils.isEmpty(lastVideoPath)) {
            setVideoPath(lastVideoPath);
        }
        userSeekTime = time;
        syncSeek();
    }

    private void syncSeek() {
        if (player != null && userSeekTime != -1) {
            player.setSeekTime(userSeekTime);
            userSeekTime = -1;
        }
    }

    public int getVideoWidth() {
        return videoWidth;
    }

    public int getVideoHeight() {
        return videoHeight;
    }

    public long getCurrentTime() {
        if (player != null) {
            return player.getCurrentTime();
        }
        return 0;
    }

    public long getTotalTime() {
        return videoTotalTime;
    }

    public boolean isPlaying() {
        if (player != null) return player.isPlaying();
        return false;
    }

    public boolean isEnd() {
        if (player != null) return player.isEnd();
        return true;
    }


    public void setPause(boolean pause) {
        if (isEnd() && !pause && !TextUtils.isEmpty(lastVideoPath)) {
            setVideoPath(lastVideoPath);
            return;
        }
        userPause = pause ? 1 : 0;
        syncPause();
    }

    private void syncPause() {
        if (player != null && userPause != -1) {
            player.setPause(userPause == 1);
        }
    }

    public void stopPlayer() {
        videoPath = null;
        currentSeekTime = 0;
        userPause = -1;
        userSeekTime = -1;
        releasePlayer();
    }

    private void releasePlayer() {
        if (player == null) return;
        handler.removeCallbacks(syncTimeStateRunnable);
        player.stopPlayerRelease();
        Log.i(TAG, "releasePlayer: enter:"+player);
        player = null;
    }

    Runnable syncTimeStateRunnable = new Runnable() {
        @Override
        public void run() {
            if (player != null) {
                if (getCurrentTime() > 0) {
                    currentSeekTime = getCurrentTime();
                }
                if (callback != null) {
                    Message msg = Message.obtain();
                    msg.what = NewPlayer.progress_state;
                    callback.handleMessage(msg);
                }
            }
            handler.postDelayed(syncTimeStateRunnable, 128);
        }
    };


    @Override
    public void surfaceCreated(@NonNull SurfaceHolder holder) {
        mHolder = holder;
        releasePlayer();
        initPlayer();
    }

    @Override
    public void surfaceDestroyed(@NonNull SurfaceHolder holder) {
        mHolder = null;
        releasePlayer();
    }

    private void init(Context context) {
        mContext = context;
        getHolder().addCallback(this);
    }

    public void release() {
        handler.removeCallbacksAndMessages(null);
        callback = null;
        getHolder().removeCallback(this);
        ViewParent parent = getParent();
        if (parent instanceof ViewGroup) {
            ((ViewGroup) parent).removeView(this);
        }
    }

    @Override
    public void surfaceRedrawNeeded(@NonNull SurfaceHolder holder) {

    }

    @Override
    public void surfaceChanged(@NonNull SurfaceHolder holder, int format, int width, int height) {

    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        if (msg.what == NewPlayer.prepare_state) {
            videoWidth = player.getWidth();
            videoHeight = player.getHeight();
            videoTotalTime = player.getTotalTime();
        }
        if (msg.what == NewPlayer.error_state) {
            lastVideoPath = null;
            resetVideoInfo();
        }
        if (msg.what == NewPlayer.end_state || msg.what == NewPlayer.error_state) {
            stopPlayer();
        }
        if (msg.what == NewPlayer.prepare_state || msg.what == NewPlayer.end_state || msg.what == NewPlayer.error_state) {
            if (callback != null) {
                callback.handleMessage(msg);
            }
        }
        return false;
    }

    private void resetVideoInfo() {
        videoTotalTime = 0;
        videoHeight = -1;
        videoWidth = -1;
    }
}

调用案例:

java 复制代码
package com.github.jasonhancn.tvcursor;

import android.annotation.SuppressLint;
import android.os.Bundle;
import android.os.Handler;
import android.os.Message;
import android.util.Log;
import android.view.Menu;
import android.view.MenuItem;
import android.view.ViewGroup;
import android.widget.EditText;
import android.widget.ProgressBar;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.app.AlertDialog;
import androidx.appcompat.app.AppCompatActivity;

import com.example.mqttdome.R;
import com.github.jasonhancn.tvcursor.util.NewPlayer;
import com.github.jasonhancn.tvcursor.util.NewPlayerView;

public class TestActivity extends AppCompatActivity implements Handler.Callback {
    private static final String TAG = "TestActivity";

    NewPlayerView playerView;
    private ProgressBar progress;

    @Override
    protected void onCreate(@Nullable Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.test_activity);
        setTitle("信发视频小组件测试");

        progress = findViewById(R.id.progressBar);

        playerView = findViewById(R.id.playerView);
        playerView.setCallback(this);
        playerView.setVideoPath("http://em.21dtv.com/songs/60052907.mkv");
    }

    @Override
    protected void onDestroy() {
        playerView.release();
        super.onDestroy();
    }

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        getMenuInflater().inflate(R.menu.test_menu, menu);
        return super.onCreateOptionsMenu(menu);
    }

    @SuppressLint("SetTextI18n")
    @Override
    public boolean onOptionsItemSelected(@NonNull MenuItem item) {
        if (item.getItemId() == R.id.pause) {
            playerView.setPause(true);
        }
        if (item.getItemId() == R.id.player) {
            playerView.setPause(false);
        }
        if (item.getItemId() == R.id.first) {
            playerView.goForward();
        }
        if (item.getItemId() == R.id.back) {
            playerView.goBack();
        }
        if (item.getItemId() == R.id.url) {
            EditText edit = new EditText(this);
            edit.setText("http://em.21dtv.com/songs/60052907.mkv");
            new AlertDialog.Builder(this)
                    .setTitle("设置地址")
                    .setView(edit)
                    .setPositiveButton("ok", (dialog, which) -> playerView.setVideoPath(edit.getText().toString())).show();
            ViewGroup.MarginLayoutParams layoutParams = (ViewGroup.MarginLayoutParams) edit.getLayoutParams();
            layoutParams.leftMargin = 38;
            layoutParams.rightMargin = 38;
            edit.setLayoutParams(layoutParams);
        }
        return super.onOptionsItemSelected(item);
    }

    @Override
    public boolean handleMessage(@NonNull Message msg) {
        if (msg.what == NewPlayer.progress_state) {
            if (playerView.getTotalTime() > 0) {
                float jd = playerView.getCurrentTime() * 1f / playerView.getTotalTime() * 100;
                progress.setProgress((int) jd);
            } else {
                progress.setProgress(0);
            }
        }
        if (msg.what == NewPlayer.end_state) {
            progress.setProgress(100);
        }
        if (msg.what == NewPlayer.error_state) {
            progress.setProgress(0);
        }
        if (msg.what == NewPlayer.prepare_state) {
            ViewGroup.LayoutParams layoutParams = playerView.getLayoutParams();
            int[] size = NewPlayer.calculateImageSize(getApplicationContext(), playerView.getVideoWidth(), playerView.getVideoHeight());
            layoutParams.width = size[0];
            layoutParams.height = size[1];
            playerView.setLayoutParams(layoutParams);
            Log.i(TAG, "handleMessage: size:" + playerView.getVideoWidth() + "; " + playerView.getVideoHeight());
        }
        return false;
    }
}

使用上面工具类,不用考虑状态问题!!

对你有用请点赞收藏,谢谢!!!

相关推荐
花开月满西楼42 分钟前
保姆级【快数学会Android端“动画“】+ 实现补间动画和逐帧动画!!!
android·前端·android studio
这儿有一堆花1 小时前
打造你的 Android 图像编辑器:深入解析 PhotoEditor 开源库
android·开源
皮皮高2 小时前
itvbox绿豆影视tvbox手机版影视APP源码分享搭建教程
android·前端·后端·开源·tv
EnzoRay2 小时前
MotionEvent
android
玲小珑3 小时前
Auto.js 入门指南(七)定时任务调度
android·前端
墨狂之逸才3 小时前
adb常用命令调试
android
YoungForYou3 小时前
Android端部署NCNN
android
移动开发者1号3 小时前
Jetpack Compose瀑布流实现方案
android·kotlin
移动开发者1号4 小时前
Android LinearLayout、FrameLayout、RelativeLayout、ConstraintLayout大混战
android·kotlin
移动开发者1号4 小时前
ListView与RecyclerView区别总结
android·kotlin