Files
jiecheng-business/mediaplayertv/src/main/java/com/pukka/videoplayertv/VideoViewTv.java

1584 lines
54 KiB
Java
Raw Normal View History

package com.pukka.videoplayertv;
import static android.view.KeyEvent.ACTION_DOWN;
import android.annotation.TargetApi;
import android.content.Context;
import android.media.AudioManager;
import android.media.MediaPlayer;
import android.net.Uri;
import android.os.Build;
import android.util.AttributeSet;
import android.util.Log;
import android.view.Gravity;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import androidx.annotation.NonNull;
import com.pukka.libcore.boxShared.BoxSharedParamsUtils;
import com.pukka.libcore.utils.ContextUtils;
import com.pukka.libcore.utils.LogUtils;
import com.pukka.videoplayertv.exoplayer.PkExoMediaPlayer;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.Map;
/**
* 视频播放控件
* 使用方法
* mMediaController = new TVmediaController(this, true);
* mVideoView = (VideoViewTv) findViewById(R.id.video_view);
* mVideoView.setVideoPath(mVideoPath);
* mVideoView.setMediaController(mMediaController);
* mVideoView.start();
*/
public class VideoViewTv extends FrameLayout implements MediaPlayerControl {
private static final String TAG = "VideoViewTv";
/**
* 本次 APK 运行期间是否已经设置过默认音量
* 用途:控制只在首次进入视频播放时设置默认音量(30%),后续切换分集、切换影片不再重置。
* 进程重启(APK 重启)后静态变量自动重置为 false,从而恢复"首次进入详情设置默认音量"的流程。
* 不使用 SharedPreferences:因为需求要求 APK 重启后重新走首次设置流程,需内存级标志位。
*/
private static boolean sDefaultVolumeApplied = false;
// 播放视频路径
private Uri mUri;
// 媒体视频headers
private Map<String, String> mHeaders;
// 播放器可能的状态
// 播放器出错状态
public static final int STATE_ERROR = -1;
// 闲置状态
public static final int STATE_IDLE = 0;
// 准备状态
public static final int STATE_PREPARING = 1;
// 准备结束状态
public static final int STATE_PREPARED = 2;
// 播放状态
public static final int STATE_PLAYING = 3;
// 暂停状态
public static final int STATE_PAUSED = 4;
// 播放结束状态
public static final int STATE_PLAYBACK_COMPLETED = 5;
// mCurrentState is a VideoView object's current state.
// mTargetState is the state that a method caller intends to reach.
// For instance, regardless the VideoView object's current state,
// calling pause() intends to bring the object to a target state
// of STATE_PAUSED.
// 播放器当前状态
private int mCurrentState = STATE_IDLE;
// 播放器下一个状态
private int mTargetState = STATE_IDLE;
// All the stuff we need for playing and showing a video
// 播放的surfaceholder接口
private IRenderView.ISurfaceHolder mSurfaceHolder = null;
// 播放器接口
private IMediaPlayer mMediaPlayer = null;
// 视频宽度
private int mVideoWidth;
// 视频高度
private int mVideoHeight;
// surface宽度
private int mSurfaceWidth;
// surface高度
private int mSurfaceHeight;
// 视频控制器
private IMediaController mMediaController;
// 视频播放完成的监听
private IMediaPlayer.OnCompletionListener mOnCompletionListener;
// 视频准备结束的监听
private IMediaPlayer.OnPreparedListener mOnPreparedListener;
private int mCurrentBufferPercentage;
// 缓冲
private IMediaPlayer.OnBufferingUpdateListener mOnBufferingUpdateListener;
// 视频播放过程中出错的监听
private IMediaPlayer.OnErrorListener mOnErrorListener;
// 视频信息列表的监听
private IMediaPlayer.OnInfoListener mOnInfoListener;
// recording the seek position while preparing
private int mSeekWhenPrepared;
// 暂停
private boolean mCanPause = true;
// 后退
private boolean mCanSeekBack = true;
// 前进
private boolean mCanSeekForward = true;
// 视频呈现方式
private int mCurrentAspectRatio = IRenderView.AR_ASPECT_WRAP_CONTENT;
// 播放视频VIEW
private IRenderView mRenderView;
private int mVideoSarNum;
private int mVideoSarDen;
// 直播[1]和点播[0]的区分
private int isLive = 0;
// 判断是否是软终端,选择不同的播放器 0是常规 , 1是软终端 2是ExoMediaPlayer
private int terminalType = 2;
/**
* 全屏模式
**/
public static final int MODE_FULL_SCREEN = 11;
/**
* 小窗口模式
**/
public static final int MODE_TINY_WINDOW = 12;
/**
* 普通模式
*/
public static final int MODE_NORMAL = 13;
private int mCurrentMode = MODE_NORMAL;
public IOnPauseListener onPauseListener;
public IOnResumeListener onResumeListener;
/**
* 是否是快进
*/
private boolean isSeekForward;
public boolean isSeekForward() {
return isSeekForward;
}
/**
* 首次播放的seek播放进度(比如说release 播放器以后要播放到指定position)
*/
private int firstPlayPosition;
public void setFirstPlayPosition(int firstPlayPosition) {
this.firstPlayPosition = firstPlayPosition;
}
public IMediaPlayer getMediaPlayer() {
LogUtils.d("getMediaPlayer: "+mMediaPlayer.toString());
return mMediaPlayer;
}
/**
* 设置成全屏播放模式
*/
public void setModeFullScreen() {
setmCurrentAspectRatio(IRenderView.AR_ASPECT_FIT_PARENT);
this.mCurrentMode = MODE_FULL_SCREEN;
// setRenderView();
FrameLayout.LayoutParams layoutParams_txt =
new FrameLayout.LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.TOP);
this.setLayoutParams(layoutParams_txt);
}
/**
* 设置成全屏播放模式
*
* @param width 全屏时的width
* @param height 全屏时的height
*/
public void setModeFullScreen(int width, int height) {
setmCurrentAspectRatio(IRenderView.AR_ASPECT_FIT_PARENT);
this.mCurrentMode = MODE_FULL_SCREEN;
mRenderView.setVideoSize(width > 0 ? width : mVideoWidth, height > 0 ? height : mVideoHeight);
}
/**
* 设置成小屏播放模式
*
* @param width 小屏时的width
* @param height 小屏时的height
*/
public void setModeTinyScreen(int width, int height) {
setmCurrentAspectRatio(IRenderView.AR_ASPECT_FIT_PARENT);
this.mCurrentMode = MODE_TINY_WINDOW;
LogUtils.d("smm -- ", "setModeTinyScreen width:"+width + " height:"+height);
mRenderView.setVideoSize(width, height);
setFocusable(false);
// 隐藏播控
if (null != mMediaController) {
mMediaController.setMediaPlayer(null);
mMediaController.hide();
mMediaController = null;
}
}
public void setmCurrentMode(int mCurrentMode) {
this.mCurrentMode = mCurrentMode;
}
/**
* 是否是全屏模式
*
* @return
*/
public boolean isFullScreen() {
return this.mCurrentMode == MODE_FULL_SCREEN;
}
/**
* 设置成小屏播放模式,且隐藏播控
*
* @param width 视频控件 宽
* @param height 视频控件 高
* @param left 左侧偏移量
* @param top 头部偏移量
*/
public void setModeTinyWindow(int width, int height, int left, int top) {
setmCurrentAspectRatio(IRenderView.AR_ASPECT_FIT_PARENT);
this.mCurrentMode = MODE_TINY_WINDOW;
FrameLayout.LayoutParams layoutParams_txt = new FrameLayout.LayoutParams(width, height, Gravity.TOP);
layoutParams_txt.setMarginStart(left);
layoutParams_txt.topMargin = top;
setLayoutParams(layoutParams_txt);
setFocusable(false);
// 隐藏播控
if (null != mMediaController) {
mMediaController.hide();
}
}
public void setTerminalType(int terminalType) {
this.terminalType = terminalType;
}
public int getTerminalType() {
return terminalType;
}
public void switchTerminal(int terminalType, String path) {
this.terminalType = terminalType;
mMediaPlayer.release();
mMediaPlayer = null;
initVideoView();
setVideoPath(path);
}
/**
* 获取播放器状态
*/
public int getCurrentState() {
return mCurrentState;
}
/**
* 构造函数
*/
public VideoViewTv(Context context) {
super(context);
initVideoView();
}
/**
* 构造函数
*/
public VideoViewTv(Context context, AttributeSet attrs) {
super(context, attrs);
initVideoView();
}
/**
* 构造函数
*/
public VideoViewTv(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initVideoView();
}
private void initVideoView() {
if (!checkAudioCapability()){
terminalType = 0;
}
if (BoxSharedParamsUtils.getInstance().getModel().equals("NL_MTK9255_D1E8_RG_828_RX50")){
//这个型号设备Guava库版本过低,使用exo会崩溃
terminalType = 0;
}
setRenderView();
mVideoWidth = 0;
mVideoHeight = 0;
mCurrentState = STATE_IDLE;
mTargetState = STATE_IDLE;
}
private boolean checkAudioCapability() {
LogUtils.d("Audio", "getModel:" +BoxSharedParamsUtils.getInstance().getModel());
try {
// 1. 先检查类是否存在
Class<?> audioCapClass = Class.forName("com.google.android.exoplayer2.audio.AudioCapabilities");
// 2. 检查方法是否存在
Method getCapabilitiesMethod = audioCapClass.getDeclaredMethod("getCapabilities", Context.class);
// 3. 调用方法
Object result = getCapabilitiesMethod.invoke(null, getContext());
return true;
} catch (ClassNotFoundException e) {
LogUtils.d("Audio", "AudioCapabilities类不存在");
return false;
} catch (NoSuchMethodException e) {
LogUtils.d("Audio", "getCapabilities方法不存在");
return false;
} catch (Throwable t) {
LogUtils.d("Audio", "其他错误:"+ t);
return false;
}
}
/**
* 设置RenderView
*/
public void setRenderView() {
LogUtils.d("smm -- ", "setRenderView");
SurfaceRenderView renderView = new SurfaceRenderView(getContext());
// 修改原视图
if (mRenderView != null) {
if (mMediaPlayer != null) {
mMediaPlayer.setDisplay(null);
}
LogUtils.d("smm --- mid -- ", "setRenderView");
View renderUIView = mRenderView.getView();
mRenderView.removeRenderCallback(mSHCallback);
mRenderView = null;
removeView(renderUIView);
mSurfaceHolder = null;
}
if (renderView == null) {
return;
}
mRenderView = renderView;
// 设置视频显示方式
renderView.setAspectRatio(mCurrentAspectRatio);
LogUtils.d("mCurrentAspectRatio=" + mCurrentAspectRatio);
if (mVideoWidth > 0 && mVideoHeight > 0) {
renderView.setVideoSize(mVideoWidth, mVideoHeight);
}
if (mVideoSarNum > 0 && mVideoSarDen > 0) {
renderView.setVideoSampleAspectRatio(mVideoSarNum, mVideoSarDen);
}
LogUtils.d("mVideoHeight=" + mVideoHeight);
View renderUIView = mRenderView.getView();
LayoutParams lp = new LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT, Gravity.CENTER);
LogUtils.d("LP -- width : " + lp.width + " height : " + lp.height);
renderUIView.setLayoutParams(lp);
// 将SurfaceRenderView添加到这个自定义控件中
addView(renderUIView);
mRenderView.addRenderCallback(mSHCallback);
LogUtils.d("smm --- end -- ", "setRenderView");
}
public void setVolume(float leftVolume, float rightVolume) {
LogUtils.d("setVolume() + leftVolume: " + leftVolume + " rightVolume: " + rightVolume
+ " mMediaPlayer= " + mMediaPlayer);
if (mMediaPlayer != null) {
mMediaPlayer.setVolume(leftVolume, rightVolume);
}
}
/**
* 打开视频
*/
@TargetApi(Build.VERSION_CODES.M)
private void openVideo() {
LogUtils.d("openVideo() + uri : " + mUri);
if (mMediaPlayer != null) {
LogUtils.d("mMediaPlayer is null");
return;
}
if (mUri == null || mSurfaceHolder == null) {
// not ready for playback just yet, will try again later
LogUtils.d("mSurfaceHolder is null");
return;
}
// we shouldn't com.pukka.common_lib.clear the target state, because somebody might have
// called start() previously
// 重复的销毁播放器,把销毁播放放到epg来做
// release(false);
// releaseError(true);
// 音频管理器
AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
if (am != null) {
// 仅在本次 APK 运行期间首次进入视频播放时设置默认音量(30%),
// 后续切换分集、切换影片保持用户当前音量不变;
// APK 重启后 sDefaultVolumeApplied 静态标志位自动重置,重新走首次设置流程
if (!sDefaultVolumeApplied) {
int maxVolume = am.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
// 使用 Math.round 四舍五入,寻找最接近 10% 的真实硬件阶数(避免向下取整导致偏差过大)
int targetVolume = Math.round(maxVolume * 0.1f);
am.setStreamVolume(AudioManager.STREAM_MUSIC, targetVolume, 0);
sDefaultVolumeApplied = true;
LogUtils.d("openVideo() set STREAM_MUSIC volume to 30%: " + targetVolume + " / " + maxVolume);
} else {
LogUtils.d("openVideo() keep current STREAM_MUSIC volume, default already applied this session");
}
}
int audioStatus = am.requestAudioFocus(focusChange -> LogUtils.d("onAudioFocusChange --XX : " + focusChange), AudioManager.STREAM_MUSIC, AudioManager.AUDIOFOCUS_GAIN);
LogUtils.d("requestAudioFocus -- : " + audioStatus);
try {
LogUtils.d("smm -- ", "openvideo -- create");
// 获得一个视频管理器
mMediaPlayer = createPlayer();
LogUtils.d("mMediaPlayer -- : " + mMediaPlayer);
if (mMediaPlayer != null) {
// a context for the subtitle renderers
// REMOVED: SubtitleController
// REMOVED: mAudioSession
// 注册监听事件
mMediaPlayer.setOnPreparedListener(mPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
mMediaPlayer.setOnCompletionListener(mCompletionListener);
mMediaPlayer.setOnErrorListener(mErrorListener);
mMediaPlayer.setOnInfoListener(mInfoListener);
mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
mMediaPlayer.setOnSeekCompleteListener(seekCompleteListener);
mCurrentBufferPercentage = 0;
LogUtils.d("mMediaPlayer -- setOn over : ");
mMediaPlayer.setDataSource(getContext(), mUri, mHeaders);
LogUtils.d("smm -00- setDataSource url is -- :" + mUri);
bindSurfaceHolder(mMediaPlayer, mSurfaceHolder);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setScreenOnWhilePlaying(true);
mMediaPlayer.prepareAsync();
LogUtils.d("prepareAsync ---- start----");
/*
* if (mHudViewHolder != null)
* mHudViewHolder.setMediaPlayer(mMediaPlayer);
*/
// REMOVED: mPendingSubtitleTracks
// we don't set the target state here either, but preserve the
// target state that was there before.
mCurrentState = STATE_PREPARING;
attachMediaController();
} else {
LogUtils.d("mMediaPlayer ---- is null----");
}
} catch (IOException ex) {
Log.w(TAG, "Unable to open content: " + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
} catch (IllegalArgumentException ex) {
Log.w(TAG, "Unable to open content: " + mUri, ex);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
} catch (IllegalStateException ex) {
ex.printStackTrace();
Log.w(TAG, "IllegalStateException: " + mUri, ex);
} catch (NullPointerException ex) {
ex.printStackTrace();
Log.w(TAG, "NullPointerException: " + ex);
} finally {
// REMOVED: mPendingSubtitleTracks.com.pukka.common_lib.clear();
}
}
/***
* 根据配置创建一个播放器
*/
// @UnstableApi
public IMediaPlayer createPlayer() {
LogUtils.i("createPlayer -- start -- new ");
IMediaPlayer mediaPlayer;
// 1为软终端 采用杭研播放器
if (terminalType == 1) {
LogUtils.d("SoftMediaPlayer() terminalType: "+terminalType);
mediaPlayer = new SoftMediaPlayer(getContext());
}else if(terminalType == 2){
LogUtils.d("PkExoMediaPlayer() terminalType: "+terminalType);
mediaPlayer = new PkExoMediaPlayer(ContextUtils.getContext());
}
else {
LogUtils.d("AndroidMediaPlayer() terminalType: "+terminalType);
mediaPlayer = new AndroidMediaPlayer();
}
LogUtils.i("createPlayer -- end");
return mediaPlayer;
}
/**
* release the media player in any state
* 复位播放器,释放播放器资源
*
* @param clearTargetState clearTargetState
**/
public void release(boolean clearTargetState) {
LogUtils.i("release");
try {
if (mMediaPlayer != null) {
LogUtils.i("release---------start---------1" + clearTargetState);
mCurrentState = STATE_IDLE;
mMediaPlayer.setDisplay(null);
// mMediaPlayer.setSurface(null);
pause();
reset();
mMediaPlayer.release();
if(mMediaPlayer != null){
if(mMediaPlayer instanceof PkExoMediaPlayer){
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
pkExoMediaPlayer.resetListeners();
}
}
mMediaPlayer = null;
mUri = null;
mVideoWidth = 0;
mVideoHeight = 0;
}
} catch (Exception e) {
releaseError(clearTargetState);
e.printStackTrace();
}
}
public void releaseError(boolean clearTargetState) {
try {
if (null != mMediaPlayer) {
mMediaPlayer = null;
mCurrentState = STATE_IDLE;
mUri = null;
if (clearTargetState) {
mTargetState = STATE_IDLE;
}
AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
am.abandonAudioFocus(focusChange -> LogUtils.d("onAudioFocusChange --YY : " + focusChange));
LogUtils.i("release---------end ----");
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 复位播放器
*/
public void reset() {
try {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
mCurrentState = STATE_IDLE;
mTargetState = STATE_IDLE;
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 关联IjkVideoView与mediaController
*/
private void attachMediaController() {
if (this.mCurrentMode == MODE_FULL_SCREEN || this.mCurrentMode == MODE_NORMAL) {
if (mMediaPlayer != null && mMediaController != null) {
// 将ijkvideoview对象传递给controller的成员变量
mMediaController.setMediaPlayer(this);
// View anchorView = this.getParent() instanceof View ?
// (View) this.getParent() : this;
ViewGroup anchorView = this.getParent() instanceof ViewGroup ? (ViewGroup) this.getParent() : this;
// ViewGroup anchorView = this;
// 把控制条附加到播放视频的SurfaceView上
mMediaController.setAnchorView(anchorView);
mMediaController.setEnabled(isInPlaybackState());
}
}
}
/**
* 为VideoView设置MediaController
**/
public void setMediaController(IMediaController controller) {
if (mMediaController != null) {
mMediaController.hide();
}
// 将AndroidMediaController对象赋值给IjkVideoView类的成员变量
mMediaController = controller;
attachMediaController();
}
public void setTouchModeEnabled(boolean isTouchMode) {
if (mMediaController instanceof TvMediaController) {
((TvMediaController) mMediaController).setTouchModeEnabled(isTouchMode);
}
}
/**
* 判断是否可以进行播放
*/
private boolean isInPlaybackState() {
return (mMediaPlayer != null && mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE
&& mCurrentState != STATE_PREPARING);
}
/**
* 判断是否可以获取总时长,当前时间等
*/
private boolean isInOperation() {
return (mMediaPlayer != null && mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE
&& mCurrentState != STATE_PREPARING && mCurrentState != STATE_PLAYBACK_COMPLETED);
}
/**
* 判断是否可以快进
*/
private boolean isInffwdState() {
return (mMediaPlayer != null && mCurrentState != STATE_ERROR && mCurrentState != STATE_IDLE
&& mCurrentState != STATE_PREPARING && mCurrentState != STATE_PLAYBACK_COMPLETED);
}
/**
* 设置视频路径
*
* @param path the path of the video.
*/
public void setVideoPath(String path) {
if(BoxSharedParamsUtils.getInstance().isNeed2http()) {
String videoUrl = path.replace("https://", "http://");
setVideoURI(Uri.parse(videoUrl));
}else {
setVideoURI(Uri.parse(path));
}
}
/**
* 停止播放 释放播放资源
*/
public void stopPlayback() {
try {
if (mMediaPlayer != null && mMediaPlayer.isPlaying()) {
mMediaPlayer.stop();
mMediaPlayer.release();
mMediaPlayer.reset();
mMediaPlayer = null;
mCurrentState = STATE_IDLE;
mTargetState = STATE_IDLE;
AudioManager am = (AudioManager) getContext().getSystemService(Context.AUDIO_SERVICE);
am.abandonAudioFocus(null);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 设置视频URI.
*
* @param uri the URI of the video.
*/
public void setVideoURI(Uri uri) {
setVideoURI(uri, null);
}
/**
* Sets video URI using specific headers.
*
* @param uri the URI of the video.
* @param headers the headers for the URI request.
* Note that the cross domain redirection is allowed by default, but that can be
* changed with key/value pairs through the headers parameter with
* "android-allow-cross-domain-redirect" as the key and "0" or "1" as the value
* to disallow or allow cross domain redirection.
*/
private void setVideoURI(Uri uri, Map<String, String> headers) {
mUri = uri;
mHeaders = headers;
mSeekWhenPrepared = 0;
// 打开视频
openVideo();
// 重新绘制画面
requestLayout();
// 重新绘制画面
invalidate();
}
public void setVideoURIFull(Uri uri, Map<String, String> headers) {
LogUtils.d("uri --YY : " + uri.toString());
mUri = uri;
mHeaders = headers;
mSeekWhenPrepared = 0;
// 打开视频
// openVideo();
// 重新绘制画面
requestLayout();
// 重新绘制画面
invalidate();
}
/**
* 设置
*/
// ***************************按键事件响应**********************************************//
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
return super.dispatchKeyEvent(event);
}
// ***************************按键事件响应**********************************************//
// ************************* 画布Callback **********************************//
/**
* surface发生变化, 执行
*/
IRenderView.IRenderCallback mSHCallback = new IRenderView.IRenderCallback() {
@Override
public void onSurfaceChanged(@NonNull IRenderView.ISurfaceHolder holder, int format, int w, int h) {
LogUtils.d("onSurfaceChanged holder : " + holder);
LogUtils.d("onSurfaceChanged : " + w + " height : " + h);
if (holder.getRenderView() != mRenderView) {
LogUtils.e("onSurfaceChanged: unmatched render callback\n");
return;
}
mSurfaceWidth = w;
mSurfaceHeight = h;
boolean isValidState = (mTargetState == STATE_PLAYING);
boolean hasValidSize = !mRenderView.shouldWaitForResize() || (mVideoWidth == w && mVideoHeight == h);
if (mMediaPlayer != null && isValidState && hasValidSize) {
if (mSeekWhenPrepared != 0) {
seekTo(mSeekWhenPrepared);
}
LogUtils.d("onSurfaceChanged()");
// 添加一个判断,如果已经在播放了,不要重复起播 -- 21/10/12
// if(mCurrentState != STATE_PLAYING){
LogUtils.d("start -- 22 --");
start();
// }
// if (mMediaController != null) {
// mMediaController.show();
// LogUtils.d(" mMediaController.show()");
// }
}
}
@Override
public void onSurfaceCreated(@NonNull IRenderView.ISurfaceHolder holder, int width, int height) {
LogUtils.d("onSurfaceCreated holder : " + holder);
LogUtils.d("onSurfaceCreated : " + width + " height : " + height);
if (holder.getRenderView() != mRenderView) {
LogUtils.e("onSurfaceCreated: unmatched render callback\n");
return;
}
mSurfaceHolder = holder;
if (mMediaPlayer != null) {
LogUtils.d("bindSurfaceHolder : 00 ");
bindSurfaceHolder(mMediaPlayer, holder);
} else {
LogUtils.d("openVideo : 00 ");
openVideo();
}
}
@Override
public void onSurfaceDestroyed(@NonNull IRenderView.ISurfaceHolder holder) {
LogUtils.d("onSurfaceDestroyed : 00 " + holder);
if (holder.getRenderView() != mRenderView) {
LogUtils.e("onSurfaceDestroyed: unmatched render callback\n");
return;
}
// after we return from this we can't use the surface any more
mSurfaceHolder = null;
// REMOVED: if (mMediaController != null) mMediaController.hide();
// REMOVED: release(true);
releaseWithoutStop();
}
};
public void releaseWithoutStop() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.setDisplay(null);
}
}
private void bindSurfaceHolder(IMediaPlayer mp, IRenderView.ISurfaceHolder holder) {
if (mp == null) {
LogUtils.d("bindSurfaceHolder -- mp -- " + mp);
return;
}
if (holder == null) {
LogUtils.d("bindSurfaceHolder -- holder -- " + holder);
mp.setDisplay(null);
return;
}
holder.bindToMediaPlayer(mp);
}
// ************************* 画布Callback **********************************//
// ******************************播放器Callbak*****************************************//
IMediaPlayer.OnVideoSizeChangedListener mSizeChangedListener = new IMediaPlayer.OnVideoSizeChangedListener() {
public void onVideoSizeChanged(IMediaPlayer mp, int width, int height, int sarNum, int sarDen) {
LogUtils.d("OnVideoSizeChangedListener --- IMediaPlayer = " + mp);
try {
if (mp != null) {
mVideoWidth = mp.getVideoWidth();
mVideoHeight = mp.getVideoHeight();
mVideoSarNum = mp.getVideoSarNum();
mVideoSarDen = mp.getVideoSarDen();
LogUtils.d(
"OnVideoSizeChangedListener --- mVideoWidth = " + mVideoWidth + "-- mVideoHeight -- : "
+ mVideoHeight + "-- mVideoSarNum -- : " + mVideoSarNum + "-- mVideoSarDen -- : "
+ mVideoSarDen);
if (mVideoWidth != 0 && mVideoHeight != 0) {
if (mRenderView != null) {
mRenderView.setVideoSize(mVideoWidth, mVideoHeight);
mRenderView.setVideoSampleAspectRatio(mVideoSarNum, mVideoSarDen);
}
// REMOVED: getHolder().setFixedSize(mVideoWidth, mVideoHeight);
LogUtils.d("OnVideoSizeChangedListener()");
requestLayout();
}
}
} catch (Exception e) {
e.printStackTrace();
LogUtils.e("OnVideoSizeChangedListener --- IMediaPlayer = " + mp);
}
}
};
IMediaPlayer.OnPreparedListener mPreparedListener = new IMediaPlayer.OnPreparedListener() {
public void onPrepared(IMediaPlayer mp) {
try {
LogUtils.d("准备好了,开始播放了。");
mCurrentState = STATE_PREPARED;
mTargetState = STATE_PLAYING;
// Get the capabilities of the player for this stream
// REMOVED: Metadata
if (mOnPreparedListener != null) {
mOnPreparedListener.onPrepared(mMediaPlayer);
}
if (ConfigPlayer.getInstance().isNeedStartFirstFrame()) {
if (mOnInfoListener != null) {
mOnInfoListener.onInfo(mMediaPlayer, 3, 0);
}
}
if (mMediaController != null) {
mMediaController.setEnabled(true);
}
mVideoWidth = mp.getVideoWidth();
mVideoHeight = mp.getVideoHeight();
LogUtils.d(
"onPrepared --- mVideoWidth -- :" + mVideoWidth + "-- mVideoHeight -- :" + mVideoHeight);
int seekToPosition = 0;
if (firstPlayPosition != 0) {
seekTo(firstPlayPosition);
setFirstPlayPosition(0);
} else {
// 定位到上次播放的位置
seekToPosition = mSeekWhenPrepared; // mSeekWhenPrepared may be changed after seekTo() call
if (seekToPosition != 0) {
seekTo(seekToPosition);
}
}
if (mVideoWidth != 0 && mVideoHeight != 0) {
// Log.i("@@@@", "video size: " + mVideoWidth +"/"+ mVideoHeight);
// REMOVED: getHolder().setFixedSize(mVideoWidth, mVideoHeight);
if (mRenderView != null) {
mRenderView.setVideoSize(mVideoWidth, mVideoHeight);
mRenderView.setVideoSampleAspectRatio(mVideoSarNum, mVideoSarDen);
// mRenderView.setVideoRotation(90);
// Log.i("wxf videowidth",mVideoWidth+" | "+mVideoHeight);
// Log.i("wxf videosarnum","mVideoSarNum="+mVideoSarNum +" mVideoSarDen="+mVideoSarDen);
if (!mRenderView.shouldWaitForResize()
|| mSurfaceWidth == mVideoWidth && mSurfaceHeight == mVideoHeight) {
// We didn't actually change the size (it was already at the size
// we need), so we won't get a "surface changed" callback, so
// start the video here instead of in the callback.
if (mTargetState == STATE_PLAYING) {
LogUtils.d("start -- 11 --");
start();
// if (mMediaController != null) {
// if (null != mMediaController && mCurrentMode != MODE_TINY_WINDOW) {
// mMediaController.show();
// }
// }
} else if (!isPlaying() && (seekToPosition != 0 || getCurrentPosition() > 0)) {
// if (mMediaController != null && mCurrentMode != MODE_TINY_WINDOW) {
// // Show the media controls when we're paused into a video and make 'em stick.
// mMediaController.show(0);
// }
}
}
}
} else {
// We don't know the video size yet, but should start anyway.
// The video size might be reported to us later.
if (mTargetState == STATE_PLAYING) {
LogUtils.d("start -- 00 --");
start();
// if (mMediaController != null) {
// mMediaController.show();
// LogUtils.d( "toggleMediaControlsVisiblity()");
// }
}
}
} catch (Exception e) {
e.printStackTrace();
LogUtils.e("OnVideoSizeChangedListener --- IMediaPlayer = " + mp);
}
}
};
private IMediaPlayer.OnCompletionListener mCompletionListener = new IMediaPlayer.OnCompletionListener() {
public void onCompletion(IMediaPlayer mp) {
mCurrentState = STATE_PLAYBACK_COMPLETED;
mTargetState = STATE_PLAYBACK_COMPLETED;
if (mMediaController != null) {
mMediaController.hide();
}
if (mOnCompletionListener != null) {
mOnCompletionListener.onCompletion(mMediaPlayer);
}
}
};
private IMediaPlayer.OnInfoListener mInfoListener = new IMediaPlayer.OnInfoListener() {
public boolean onInfo(IMediaPlayer mp, int arg1, int arg2) {
if (mOnInfoListener != null) {
mOnInfoListener.onInfo(mp, arg1, arg2);
}
// LogUtils.d( "mediaInfo - arg1: " + arg1);
// LogUtils.d( "mediaInfo - arg2: " + arg2);
// switch (arg1) {
// case IMediaPlayer.MEDIA_INFO_VIDEO_TRACK_LAGGING:
// LogUtils.d( "MEDIA_INFO_VIDEO_TRACK_LAGGING:");
// break;
// case IMediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START:
// LogUtils.d( "MEDIA_INFO_VIDEO_RENDERING_START:");
// break;
// case IMediaPlayer.MEDIA_INFO_BUFFERING_START:
// LogUtils.d( "MEDIA_INFO_BUFFERING_START:");
// break;
// case IMediaPlayer.MEDIA_INFO_BUFFERING_END:
// LogUtils.d( "MEDIA_INFO_BUFFERING_END:");
// break;
// case IMediaPlayer.MEDIA_INFO_NETWORK_BANDWIDTH:
// LogUtils.d( "MEDIA_INFO_NETWORK_BANDWIDTH: " + arg2);
// break;
// case IMediaPlayer.MEDIA_INFO_BAD_INTERLEAVING:
// LogUtils.d( "MEDIA_INFO_BAD_INTERLEAVING:");
// break;
// case IMediaPlayer.MEDIA_INFO_NOT_SEEKABLE:
// LogUtils.d( "MEDIA_INFO_NOT_SEEKABLE:");
// break;
// case IMediaPlayer.MEDIA_INFO_METADATA_UPDATE:
// LogUtils.d( "MEDIA_INFO_METADATA_UPDATE:");
// break;
// case IMediaPlayer.MEDIA_INFO_UNSUPPORTED_SUBTITLE:
// LogUtils.d( "MEDIA_INFO_UNSUPPORTED_SUBTITLE:");
// break;
// case IMediaPlayer.MEDIA_INFO_SUBTITLE_TIMED_OUT:
// LogUtils.d( "MEDIA_INFO_SUBTITLE_TIMED_OUT:");
// break;
// case IMediaPlayer.MEDIA_INFO_VIDEO_ROTATION_CHANGED:
// //mVideoRotationDegree = arg2;
// LogUtils.d( "MEDIA_INFO_VIDEO_ROTATION_CHANGED: " + arg2);
// if (mRenderView != null)
// mRenderView.setVideoRotation(arg2);
// break;
// case IMediaPlayer.MEDIA_INFO_AUDIO_RENDERING_START:
// LogUtils.d( "MEDIA_INFO_AUDIO_RENDERING_START:");
// break;
// default:
// // throw new IllegalStateException("Unexpected value: " + arg1);
// }
return true;
}
};
private IMediaPlayer.OnErrorListener mErrorListener = new IMediaPlayer.OnErrorListener() {
public boolean onError(IMediaPlayer mp, int framework_err, int impl_err) {
LogUtils.d("Error: " + framework_err + "," + impl_err);
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
if (mMediaController != null) {
mMediaController.hide();
}
/* If an error handler has been supplied, use it and finish. */
if (mOnErrorListener != null) {
if (impl_err == 3516 || impl_err == 3515) {
// 组播切单播,组播播放报错,切单播
framework_err = impl_err;
}
if (mOnErrorListener.onError(mMediaPlayer, framework_err, impl_err)) {
return true;
}
}
/*
* Otherwise, pop up an error dialog so the user knows that
* something bad has happened. Only try and pop up the dialog
* if we're attached to a window. When we're going away and no
* longer have a window, don't bother showing the user an error.
*/
// if (getWindowToken() != null) {
// Resources r = getContext().getResources();
// int messageId;
//
// if (framework_err == MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK) {
// messageId = R.string.VideoView_error_text_invalid_progressive_playback;
// } else {
// messageId = R.string.VideoView_error_text_unknown;
// }
//
// new AlertDialog.Builder(getContext())
// .setMessage(messageId)
// .setPositiveButton(R.string.VideoView_error_button,
// new DialogInterface.OnClickListener() {
// public void onClick(DialogInterface dialog, int whichButton) {
// /* If we get here, there is no onError listener, so
// * at least inform them that the video is over.
// */
// if (mOnCompletionListener != null) {
// mOnCompletionListener.onCompletion(mMediaPlayer);
// }
// }
// })
// .setCancelable(false)
// .show();
// }
return true;
}
};
private IMediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener =
new IMediaPlayer.OnBufferingUpdateListener() {
public void onBufferingUpdate(IMediaPlayer mp, int percent) {
mCurrentBufferPercentage = percent;
LogUtils.d("onBufferingUpdate ---- : "+percent);
// if (mOnBufferingUpdateListener != null) {
// mOnBufferingUpdateListener.onBufferingUpdate(mp, percent);
// }
}
};
private IMediaPlayer.OnSeekCompleteListener seekCompleteListener = new IMediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(IMediaPlayer mp) {
}
};
// 暂停
public interface IOnPauseListener {
public void onPause();
}
// 继续播放
public interface IOnResumeListener {
public void onResume();
}
// ************************************播放器Callbak***********************************//
/// ** 媒体控制器 MediaPlayerControl **////
/**
* MediaPlayerControl的start() 方法
* 播放
*/
@Override
public void start() {
LogUtils.d(
"start: " + " mMediaPlayer != null " + (mMediaPlayer != null) + ": mCurrentState: " + mCurrentState);
if (isInPlaybackState()) {
//
LogUtils.d("点击了播放 : " + mCurrentState);
mMediaPlayer.start();
mCurrentState = STATE_PLAYING;
LogUtils.d("start()");
// if (null != mMediaController && mCurrentMode != MODE_TINY_WINDOW) {
// mMediaController.show();
// }
}
mTargetState = STATE_PLAYING;
}
/**
* MediaPlayerControl的pause() 方法
* 暂定
*/
@Override
public void pause() {
try {
if (isInPlaybackState()) {
if (mMediaPlayer.isPlaying()) {
LogUtils.d("pause : ");
mMediaPlayer.pause();
mCurrentState = STATE_PAUSED;
}
}
} catch (Exception e) {
e.printStackTrace();
}
mTargetState = STATE_PAUSED;
// mMediaController.hide();
// if (null != mMediaController) {
// mMediaController.show(0);
// }
}
/**
* MediaPlayerControl的pause() 方法
* 暂定
*/
public void pause(boolean isShow) {
if (isInPlaybackState()) {
if (mMediaPlayer.isPlaying()) {
mMediaPlayer.pause();
mCurrentState = STATE_PAUSED;
}
}
mTargetState = STATE_PAUSED;
// mMediaController.hide();
if (isShow) {
mMediaController.show(0);
}
}
/**
* MediaPlayerControl的getDuration方法
* 时长
*/
@Override
public int getDuration() {
/**
* 会有出现一直error -38 报错的情况,
* Attempt to call getDuration without a valid mediaplayer
* 改 isInPlaybackState 为 isInOperation
*/
if (isInOperation()) {
// if(isLive == 1){
// return 14400 * 1000;
// }
LogUtils.d("getDuration:" + mCurrentState);
return (int) mMediaPlayer.getDuration();
}
return -1;
}
/**
* MediaPlayerControl的getCurrentPosition方法
* 播放位置
*/
@Override
public int getCurrentPosition() {
if (isInOperation()) {
// if(isLive == 1){
// return 14400 * 1000;
// }
LogUtils.d("getCurrentPosition:" + mCurrentState);
return (int) mMediaPlayer.getCurrentPosition();
}
return 0;
}
/**
* 获取对应播放进度
*
* @return 返回进度
* @throws Exception 获取异常
*/
public long getMediaPlayerCurrentPosition() throws Exception {
return mMediaPlayer.getCurrentPosition();
}
/**
* MediaPlayerControl的seekTo方法
* 播放位置
*/
@Override
public void seekTo(int msec) {
try {
if (isInPlaybackState()) {
LogUtils.d("seekTo : " + mCurrentState);
Log.i("wxf", "seekto====" + msec);
mMediaPlayer.seekTo(msec);
mSeekWhenPrepared = 0;
} else {
mSeekWhenPrepared = msec;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void seekTo(int msec,boolean isPause) {
try {
if (isInPlaybackState()) {
LogUtils.d("seekTo : " + mCurrentState);
Log.i("wxf", "seekto====" + msec);
mMediaPlayer.pause();
mMediaPlayer.seekTo(msec);
mSeekWhenPrepared = 0;
} else {
mSeekWhenPrepared = msec;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public boolean isSwitchDirectionEnable() {
if (mCurrentState == STATE_PLAYING || mCurrentState == STATE_PREPARING || mCurrentState == STATE_PREPARED) {
return true;
}
return false;
}
/**
* MediaPlayerControl的isPlaying()方法
* 判断是否正在播放
*/
@Override
public boolean isPlaying() {
try {
return isInPlaybackState() && mMediaPlayer.isPlaying();
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
/**
* MediaPlayerControl的getBufferPercentage方法
* 缓冲
*/
@Override
public int getBufferPercentage() {
if (mMediaPlayer != null) {
return mCurrentBufferPercentage;
}
return 0;
}
/***
* 不销毁播放器,切换到另一个频道
*/
public void switchChannel(String channelUrl) {
if (mMediaPlayer != null) {
try {
// mMediaPlayer.stop();
mMediaPlayer.reset();
mUri = Uri.parse(channelUrl);
mMediaPlayer.setOnPreparedListener(mPreparedListener);
mMediaPlayer.setOnVideoSizeChangedListener(mSizeChangedListener);
mMediaPlayer.setOnCompletionListener(mCompletionListener);
mMediaPlayer.setOnErrorListener(mErrorListener);
mMediaPlayer.setOnInfoListener(mInfoListener);
mMediaPlayer.setOnBufferingUpdateListener(mBufferingUpdateListener);
mMediaPlayer.setOnSeekCompleteListener(seekCompleteListener);
mCurrentBufferPercentage = 0;
mMediaPlayer.setDataSource(getContext(), mUri, mHeaders);
bindSurfaceHolder(mMediaPlayer, mSurfaceHolder);
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mMediaPlayer.setScreenOnWhilePlaying(true);
mMediaPlayer.prepareAsync();
/*
* if (mHudViewHolder != null)
* mHudViewHolder.setMediaPlayer(mMediaPlayer);
*/
// REMOVED: mPendingSubtitleTracks
// we don't set the target state here either, but preserve the
// target state that was there before.
mCurrentState = STATE_PREPARING;
// attachMediaController();
// if (null != mMediaController && mCurrentMode !=MODE_TINY_WINDOW) {
// mMediaController.show();
// }
} catch (IOException e) {
e.printStackTrace();
mCurrentState = STATE_ERROR;
mTargetState = STATE_ERROR;
mErrorListener.onError(mMediaPlayer, MediaPlayer.MEDIA_ERROR_UNKNOWN, 0);
}
}
}
/**
* 判断是否可以暂停
*/
@Override
public boolean canPause() {
return mCanPause;
}
/**
* 判断是否可以倒退
*/
@Override
public boolean canSeekBackward() {
return mCanSeekBack;
}
/**
* 判断是否可以前进
*/
@Override
public boolean canSeekForward() {
return mCanSeekForward;
}
/**
* 获取当前模式
*/
@Override
public int getCurrentMode() {
return mCurrentMode;
}
public void setCurrentMode(int mCurrentMode) {
this.mCurrentMode = mCurrentMode;
}
/**
* 设置视频显示方式
**/
public void setmCurrentAspectRatio(int mCurrentAspectRatio) {
this.mCurrentAspectRatio = mCurrentAspectRatio;
if (mRenderView != null) {
mRenderView.setAspectRatio(mCurrentAspectRatio);
}
}
public void setmVideoWidth(int mVideoWidth) {
this.mVideoWidth = mVideoWidth;
}
public void setmVideoHeight(int mVideoHeight) {
Log.d("onSurfaceChanged -- ", "setmVideoHeight -- " + mVideoHeight);
this.mVideoHeight = mVideoHeight;
// setLayoutParams(new RelativeLayout.LayoutParams(mVideoWidth,this.mVideoWidth));
// if (mRenderView != null) {
// mRenderView.setVideoSize(mVideoWidth, mVideoHeight);
// }
// requestLayout();
// //重新绘制画面
// invalidate();
}
/**
* 设置播放器的位置和宽高并刷新重新绘制画面
*/
public void setVideoPositionSize() {
// if (mRenderView != null) {
// mRenderView.setVideoSize(mVideoWidth, mVideoHeight);
// }
requestLayout();
// 重新绘制画面
invalidate();
}
/**
* 区分是直播还是点播
* 直播 1 点播 0
*/
public void setIsLive(int isLive) {
this.isLive = isLive;
}
/// ** 媒体控制器 MediaPlayerControl **////
public void setmOnCompletionListener(IMediaPlayer.OnCompletionListener mOnCompletionListener) {
this.mOnCompletionListener = mOnCompletionListener;
}
public void setmSizeChangedListener(IMediaPlayer.OnVideoSizeChangedListener mSizeChangedListener) {
this.mSizeChangedListener = mSizeChangedListener;
}
public void setmPreparedListener(IMediaPlayer.OnPreparedListener mPreparedListener) {
this.mOnPreparedListener = mPreparedListener;
}
public void setmCompletionListener(IMediaPlayer.OnCompletionListener mCompletionListener) {
this.mCompletionListener = mCompletionListener;
}
public void setmInfoListener(IMediaPlayer.OnInfoListener mInfoListener) {
this.mOnInfoListener = mInfoListener;
}
public void setmErrorListener(IMediaPlayer.OnErrorListener mErrorListener) {
this.mErrorListener = mErrorListener;
}
public void setmBufferingUpdateListener(IMediaPlayer.OnBufferingUpdateListener mBufferingUpdateListener) {
this.mBufferingUpdateListener = mBufferingUpdateListener;
}
public void setmSeekListener(IMediaPlayer.OnSeekCompleteListener seekListener) {
this.seekCompleteListener = seekListener;
}
public void setIOnPauseListener(IOnPauseListener onPauseListener) {
this.onPauseListener = onPauseListener;
}
public void setIOnResumeListener(IOnResumeListener onResumeListener) {
this.onResumeListener = onResumeListener;
}
public void removeListener(){
LogUtils.d("removeListener() ====");
mOnCompletionListener = null;
mSizeChangedListener = null;
mOnPreparedListener = null;
mCompletionListener = null;
mOnInfoListener = null;
mErrorListener = null;
mBufferingUpdateListener = null;
seekCompleteListener = null;
onPauseListener = null;
onResumeListener = null;
if(mMediaPlayer != null){
if(mMediaPlayer instanceof PkExoMediaPlayer){
PkExoMediaPlayer pkExoMediaPlayer = (PkExoMediaPlayer)mMediaPlayer;
pkExoMediaPlayer.resetListeners();
}
}
}
/**
* 获取当前mediaplayer
*/
public IMediaPlayer getMediaPlayerName() {
if (mMediaPlayer != null) {
return mMediaPlayer;
}
return null;
}
/**
* 返回正在播放的url
*
* @return
*/
public String getUrl() {
if (mUri != null) {
return String.valueOf(mUri);
}
return null;
}
/**
* 设置是否循环播放
*/
public void setLoopPlay() {
if (mMediaPlayer != null) {
mMediaPlayer.setLooping(true);
}
}
public void setLoopPlay(boolean isLoop) {
if (mMediaPlayer != null) {
mMediaPlayer.setLooping(isLoop);
}
}
/**
* 清除掉surfaceView
*/
public void clearSurfaceView() {
try {
if (mRenderView != null) {
LogUtils.d("clearSurfaceView -- : " + mRenderView);
View renderUIView = mRenderView.getView();
mRenderView.removeRenderCallback(mSHCallback);
LogUtils.d("clearSurfaceView --renderUIView-- : " + renderUIView);
LogUtils.d("clearSurfaceView --this-- : " + this);
if (renderUIView != null) {
removeView(renderUIView);
}
mRenderView = null;
mSurfaceHolder = null;
}
} catch (Exception e) {
e.printStackTrace();
}
}
public void surfaceHolderIsNull(){
LogUtils.d("surfaceHolderIsNull path: " + mUri.getPath());
LogUtils.d("surfaceHolderIsNull mSurfaceHolder: " + mSurfaceHolder);
LogUtils.d("surfaceHolderIsNull mRenderView: " + mRenderView);
if (mSurfaceHolder==null){
setVideoPath(mUri.getPath());
}
}
public void setSpeed(float speed){
if(mMediaPlayer != null) {
mMediaPlayer.setSpeed(speed);
}
}
public float getSpeed(){
if(mMediaPlayer != null) {
return mMediaPlayer.getSpeed();
}else{
return 0f;
}
}
}