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;
    }
}

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

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

相关推荐
长亭外的少年7 小时前
Kotlin 编译失败问题及解决方案:从守护进程到 Gradle 配置
android·开发语言·kotlin
建群新人小猿9 小时前
会员等级经验问题
android·开发语言·前端·javascript·php
1024小神11 小时前
tauri2.0版本开发苹果ios和安卓android应用,环境搭建和最后编译为apk
android·ios·tauri
兰琛11 小时前
20241121 android中树结构列表(使用recyclerView实现)
android·gitee
Y多了个想法11 小时前
RK3568 android11 适配敦泰触摸屏 FocalTech-ft5526
android·rk3568·触摸屏·tp·敦泰·focaltech·ft5526
NotesChapter12 小时前
Android吸顶效果,并有着ViewPager左右切换
android
_祝你今天愉快14 小时前
分析android :The binary version of its metadata is 1.8.0, expected version is 1.5.
android
暮志未晚Webgl14 小时前
109. UE5 GAS RPG 实现检查点的存档功能
android·java·ue5
麦田里的守望者江14 小时前
KMP 中的 expect 和 actual 声明
android·ios·kotlin
Dnelic-14 小时前
解决 Android 单元测试 No tests found for given includes:
android·junit·单元测试·问题记录·自学笔记