一、简介
高度自定义的安卓视频框架 MediaPlayer exoplayer ijkplayer ffmpeg
目前最新的版本是 7.7.0。
GitHub:JZVideo
中文文档:JZVD文档
特点
- 可以完全自定义UI和任何功能。
- 很容易切换播放引擎,支持的视频格式和协议取决于播放引擎,android.media.MediaPlayer或者ijkplayer等。
- 可以检测列表的滑动。
- 可以嵌入到多个控件中,比如ListView、Fragment,RecyclerView等等,多重嵌套也可以。
- 可以实现全屏播放和小窗播放。
- 可以在加载、暂停和播放等状态下切换全屏和退出全屏。
- 可以进行屏幕适配,铺满全屏或者全屏裁剪。
- 好像还支持重力感应自动横屏。
- 全屏后可以用手势修改进度和音量。
- Home键退出界面暂停播放,返回界面的时候继续播放。
二、框架引入
1. 在 app/build.gradle 中添加如下依赖:
implementation 'cn.jzvd:jiaozivideoplayer:7.7.0'
2. 在清单文件中添加网络权限:
<!-- 网络 -->
<uses-permission android:name="android.permission.INTERNET" />
三、播放在线视频
1. 效果图
2. 使用
1). 布局中添加如下控件
<cn.jzvd.JzvdStd
android:id="@+id/jz_video"
android:layout_width="match_parent"
android:layout_height="200dp"/>
2).设置视频地址、缩略图地址、标题
public class MainActivity extends BaseActivity {
private String videoUrl = "https://v-cdn.zjol.com.cn/280443.mp4";
@Override
protected int getLayoutId() {
return R.layout.activity_main;
}
@Override
protected void init(Bundle savedInstanceState) {
initView();
}
private void initView() {
JzvdStd videoPlayer = findViewById(R.id.jz_video);
videoPlayer.setUp(videoUrl,"追龙");
videoPlayer.posterImageView.setImageResource(R.mipmap.ic_cover);
// videoPlayer.startVideo();
}
@Override
public void onBackPressed() {
if (Jzvd.backPress()) {
return;
}
super.onBackPressed();
}
@Override
protected void onPause() {
super.onPause();
Jzvd.releaseAllVideos();
}
}
四、播放本地视频
1. 效果图
2. 准备本地小视频
3. JzvdStdAssert.java
/**
* Created on 2022/3/10 14:51
*
* @author Gong Youqiang
*/
public class JzvdStdAssert extends JzvdStd {
public JzvdStdAssert(Context context) {
super(context);
}
public JzvdStdAssert(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public void onPrepared() {
state = STATE_PREPARED;
if (!preloading) {
mediaInterface.start();
preloading = false;
}
onStatePlaying();
}
}
4. 布局文件
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/black"
android:orientation="vertical"
tools:context=".machine.MachineActivity">
<com.hjq.bar.TitleBar
android:id="@+id/title_bar"
android:layout_width="match_parent"
android:background="@color/purple_500"
android:layout_height="?android:attr/actionBarSize"
app:title="@string/app_video_title"
app:titleSize="18sp"
app:titleStyle="bold"
app:backButton="true"
app:leftIcon="@mipmap/ic_back"
app:titleColor="@color/white"/>
<com.hkt.devicedetection.video.JzvdStdAssert
android:id="@+id/jz_video"
android:layout_gravity="center"
android:layout_width="600dp"
android:layout_height="260dp"/>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<TextView
android:id="@+id/tv_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="14sp"
android:textColor="@color/white"
android:text="@string/app_video_desc"
android:layout_alignParentBottom="true"
android:layout_centerHorizontal="true"
android:layout_marginBottom="120dp"
android:layout_marginStart="15dp"
android:layout_marginEnd="15dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_above="@id/tv_text"
android:layout_centerHorizontal="true"
android:layout_alignParentBottom="true"
android:gravity="center"
android:layout_marginBottom="30dp"
android:layout_marginTop="20dp">
<ImageView
android:id="@+id/iv_machine_no"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:padding="8dp"
android:background="@drawable/shape_image_bg"
android:src="@mipmap/ic_home_error"/>
<ImageView
android:id="@+id/iv_machine_ok"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="80dp"
android:padding="8dp"
android:background="@drawable/shape_image_bg"
android:src="@mipmap/ic_home_ok"/>
</LinearLayout>
</RelativeLayout>
</LinearLayout>
5. JZMediaSystemAssertFolder.java
/**
* Created on 2022/3/10 11:39
*
* @author Gong Youqiang
*/
public class JZMediaSystemAssertFolder extends JZMediaInterface implements
MediaPlayer.OnPreparedListener, MediaPlayer.OnCompletionListener,
MediaPlayer.OnBufferingUpdateListener, MediaPlayer.OnSeekCompleteListener,
MediaPlayer.OnErrorListener, MediaPlayer.OnInfoListener,
MediaPlayer.OnVideoSizeChangedListener {
public MediaPlayer mediaPlayer;
public JZMediaSystemAssertFolder(Jzvd jzvd) {
super(jzvd);
}
@Override
public void prepare() {
release();
mMediaHandlerThread = new HandlerThread("JZVD");
mMediaHandlerThread.start();
mMediaHandler = new Handler(mMediaHandlerThread.getLooper());//主线程还是非主线程,就在这里
handler = new Handler();
mMediaHandler.post(() -> {
try {
mediaPlayer = new MediaPlayer();
mediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC);
mediaPlayer.setLooping(jzvd.jzDataSource.looping);
mediaPlayer.setOnPreparedListener(JZMediaSystemAssertFolder.this);
mediaPlayer.setOnCompletionListener(JZMediaSystemAssertFolder.this);
mediaPlayer.setOnBufferingUpdateListener(JZMediaSystemAssertFolder.this);
mediaPlayer.setScreenOnWhilePlaying(true);
mediaPlayer.setOnSeekCompleteListener(JZMediaSystemAssertFolder.this);
mediaPlayer.setOnErrorListener(JZMediaSystemAssertFolder.this);
mediaPlayer.setOnInfoListener(JZMediaSystemAssertFolder.this);
mediaPlayer.setOnVideoSizeChangedListener(JZMediaSystemAssertFolder.this);
//two lines are different
AssetFileDescriptor assetFileDescriptor = jzvd.getContext().getAssets().openFd(jzvd.jzDataSource.getCurrentUrl().toString());
mediaPlayer.setDataSource(assetFileDescriptor.getFileDescriptor(), assetFileDescriptor.getStartOffset(), assetFileDescriptor.getLength());
mediaPlayer.prepareAsync();
mediaPlayer.setSurface(new Surface(jzvd.textureView.getSurfaceTexture()));
} catch (Exception e) {
e.printStackTrace();
}
});
}
@Override
public void start() {
mMediaHandler.post(() -> mediaPlayer.start());
}
@Override
public void pause() {
mMediaHandler.post(() -> mediaPlayer.pause());
}
@Override
public boolean isPlaying() {
return mediaPlayer.isPlaying();
}
@Override
public void seekTo(long time) {
mMediaHandler.post(() -> {
try {
mediaPlayer.seekTo((int) time);
} catch (IllegalStateException e) {
e.printStackTrace();
}
});
}
@Override
public void release() {
if (mMediaHandler != null && mMediaHandlerThread != null && mediaPlayer != null) {//不知道有没有妖孽
HandlerThread tmpHandlerThread = mMediaHandlerThread;
MediaPlayer tmpMediaPlayer = mediaPlayer;
JZMediaInterface.SAVED_SURFACE = null;
mMediaHandler.post(() -> {
tmpMediaPlayer.release();//release就不能放到主线程里,界面会卡顿
tmpHandlerThread.quit();
});
mediaPlayer = null;
}
}
@Override
public long getCurrentPosition() {
if (mediaPlayer != null) {
return mediaPlayer.getCurrentPosition();
} else {
return 0;
}
}
@Override
public long getDuration() {
if (mediaPlayer != null) {
return mediaPlayer.getDuration();
} else {
return 0;
}
}
@Override
public void setVolume(float leftVolume, float rightVolume) {
if (mMediaHandler == null) return;
mMediaHandler.post(() -> {
if (mediaPlayer != null) mediaPlayer.setVolume(leftVolume, rightVolume);
});
}
@RequiresApi(api = Build.VERSION_CODES.M)
@Override
public void setSpeed(float speed) {
PlaybackParams pp = mediaPlayer.getPlaybackParams();
pp.setSpeed(speed);
mediaPlayer.setPlaybackParams(pp);
}
@Override
public void onPrepared(MediaPlayer mediaPlayer) {
handler.post(() -> jzvd.onPrepared());//如果是mp3音频,走这里
}
@Override
public void onCompletion(MediaPlayer mediaPlayer) {
handler.post(() -> jzvd.onCompletion());
}
@Override
public void onBufferingUpdate(MediaPlayer mediaPlayer, final int percent) {
handler.post(() -> jzvd.setBufferProgress(percent));
}
@Override
public void onSeekComplete(MediaPlayer mediaPlayer) {
handler.post(() -> jzvd.onSeekComplete());
}
@Override
public boolean onError(MediaPlayer mediaPlayer, final int what, final int extra) {
handler.post(() -> jzvd.onError(what, extra));
return true;
}
@Override
public boolean onInfo(MediaPlayer mediaPlayer, final int what, final int extra) {
handler.post(() -> {
if (what == MediaPlayer.MEDIA_INFO_VIDEO_RENDERING_START) {
if (jzvd.state == Jzvd.STATE_PREPARING
|| jzvd.state == Jzvd.STATE_PREPARING_CHANGE_URL) {
jzvd.onPrepared();
}
} else {
jzvd.onInfo(what, extra);
}
});
return false;
}
@Override
public void onVideoSizeChanged(MediaPlayer mediaPlayer, int width, int height) {
handler.post(() -> jzvd.onVideoSizeChanged(width, height));
}
@Override
public void setSurface(Surface surface) {
mediaPlayer.setSurface(surface);
}
@Override
public void onSurfaceTextureAvailable(SurfaceTexture surface, int width, int height) {
Log.i("GD", "======= onSurfaceTextureAvailable =======");
if (SAVED_SURFACE == null) {
SAVED_SURFACE = surface;
prepare();
} else {
jzvd.textureView.setSurfaceTexture(SAVED_SURFACE);
}
}
@Override
public void onSurfaceTextureSizeChanged(SurfaceTexture surface, int width, int height) {
}
@Override
public boolean onSurfaceTextureDestroyed(SurfaceTexture surface) {
return false;
}
@Override
public void onSurfaceTextureUpdated(SurfaceTexture surface) {
}
}
6. VideoActivity.java
public class VideoActivity extends BaseActivity {
@BindView(R.id.title_bar)
TitleBar mTitleBar;
@BindView(R.id.jz_video)
JzvdStdAssert mJzVideo;
public String localVideoPath;
@Override
protected int getLayoutId() {
return R.layout.activity_video;
}
@Override
protected void initView() {
mTitleBar.setOnTitleBarListener(new OnTitleBarListener() {
@Override
public void onLeftClick(View view) {
finish();
}
@Override
public void onTitleClick(View view) {
}
@Override
public void onRightClick(View view) {
}
});
localVideoPath = Environment.getExternalStorageDirectory().getAbsolutePath() + "/DCIM/Camera/activity_local_video.mp4";
//checkPermission
int permission = ActivityCompat.checkSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE");
if (permission != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, new String[]{
"android.permission.READ_EXTERNAL_STORAGE",
"android.permission.WRITE_EXTERNAL_STORAGE"}, 1);
}
//cp video 防止视频被意外删除
cpAssertVideoToLocalPath();
mJzVideo.setUp("local_video.mp4", "4K视频", JzvdStd.SCREEN_NORMAL, JZMediaSystemAssertFolder.class);
mJzVideo.posterImageView.setImageResource(R.mipmap.ic_cover);
}
@OnClick({R.id.iv_machine_no,R.id.iv_machine_ok})
public void clicked(View view) {
switch (view.getId()) {
case R.id.iv_machine_no:
MessageBean messageBean = new MessageBean();
messageBean.setItemNum(5);
messageBean.setStatus(500);
EventBus.getDefault().post(messageBean);
readyGo(MainActivity.class);
finish();
break;
case R.id.iv_machine_ok:
MessageBean message = new MessageBean();
message.setItemNum(5);
message.setStatus(200);
EventBus.getDefault().post(message);
readyGo(MainActivity.class);
finish();
break;
default:
break;
}
}
public void cpAssertVideoToLocalPath() {
if (new File(localVideoPath).exists()) return;
try {
InputStream myInput;
OutputStream myOutput = new FileOutputStream(localVideoPath);
myInput = this.getAssets().open("local_video.mp4");
byte[] buffer = new byte[1024];
int length = myInput.read(buffer);
while (length > 0) {
myOutput.write(buffer, 0, length);
length = myInput.read(buffer);
}
myOutput.flush();
myInput.close();
myOutput.close();
Toast.makeText(this, "cp from assert to local path succ", Toast.LENGTH_SHORT).show();
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void onBackPressed() {
if (Jzvd.backPress()) {
return;
}
super.onBackPressed();
}
@Override
protected void onPause() {
super.onPause();
JZUtils.clearSavedProgress(this, null);
Jzvd.releaseAllVideos();
}
@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
if (requestCode == 1) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
cpAssertVideoToLocalPath();
} else {
finish();
}
}
}
}
五、播放本地音频
1. 效果图
2. JzvdStdMp3.java
/**
* Created on 2022/3/14 14:56
*
* @author Gong Youqiang
*/
public class JzvdStdMp3 extends JzvdStd {
public JzvdStdMp3(Context context) {
super(context);
}
public JzvdStdMp3(Context context, AttributeSet attrs) {
super(context, attrs);
}
@Override
public int getLayoutId() {
return R.layout.jz_layout_mp3;
}
@Override
public void onClick(View v) {
if (v.getId() == cn.jzvd.R.id.poster &&
(state == STATE_PLAYING ||
state == STATE_PAUSE)) {
onClickUiToggle();
} else if (v.getId() == R.id.fullscreen) {
} else {
super.onClick(v);
}
}
//changeUiTo 真能能修改ui的方法
@Override
public void changeUiToNormal() {
super.changeUiToNormal();
}
@Override
public void changeUiToPreparing() {
super.changeUiToPreparing();
}
@Override
public void changeUiToPlayingShow() {
super.changeUiToPlayingShow();
posterImageView.setVisibility(View.VISIBLE);
}
@Override
public void changeUiToPlayingClear() {
super.changeUiToPlayingClear();
posterImageView.setVisibility(View.VISIBLE);
}
@Override
public void changeUiToPauseShow() {
super.changeUiToPauseShow();
posterImageView.setVisibility(View.VISIBLE);
}
@Override
public void changeUiToPauseClear() {
super.changeUiToPauseClear();
posterImageView.setVisibility(View.VISIBLE);
}
@Override
public void changeUiToComplete() {
super.changeUiToComplete();
}
@Override
public void changeUiToError() {
super.changeUiToError();
}
}
3. jz_layout_mp3.xml
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@android:color/black"
android:descendantFocusability="blocksDescendants">
<FrameLayout
android:id="@+id/surface_container"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>
<ImageView
android:id="@+id/poster"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentBottom="true"
android:background="#000000"
android:scaleType="fitCenter" />
<LinearLayout
android:id="@+id/layout_bottom"
android:layout_width="match_parent"
android:layout_height="50dp"
android:layout_alignParentBottom="true"
android:background="@drawable/jz_bottom_bg"
android:gravity="center_vertical"
android:orientation="horizontal"
android:visibility="invisible">
<TextView
android:id="@+id/current"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="14dp"
android:text="00:00"
android:textColor="#ffffff" />
<SeekBar
android:id="@+id/bottom_seek_progress"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_weight="1.0"
android:background="@null"
android:max="100"
android:maxHeight="1dp"
android:minHeight="1dp"
android:paddingLeft="12dp"
android:paddingTop="8dp"
android:paddingRight="12dp"
android:paddingBottom="8dp"
android:progressDrawable="@drawable/jz_bottom_seek_progress"
android:thumb="@drawable/jz_bottom_seek_poster" />
<TextView
android:id="@+id/total"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="00:00"
android:textColor="#ffffff" />
<TextView
android:id="@+id/clarity"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:clickable="true"
android:paddingLeft="20dp"
android:text="clarity"
android:textAlignment="center"
android:textColor="#ffffff" />
<ImageView
android:id="@+id/fullscreen"
android:layout_width="52.5dp"
android:layout_height="fill_parent"
android:paddingLeft="14dp"
android:paddingRight="14dp"
android:scaleType="centerInside"
android:src="@drawable/jz_enlarge" />
</LinearLayout>
<ProgressBar
android:id="@+id/bottom_progress"
style="?android:attr/progressBarStyleHorizontal"
android:layout_width="match_parent"
android:layout_height="1.5dp"
android:layout_alignParentBottom="true"
android:max="100"
android:progressDrawable="@drawable/jz_bottom_progress" />
<ImageView
android:id="@+id/back_tiny"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_marginLeft="6dp"
android:layout_marginTop="6dp"
android:background="@drawable/jz_click_back_tiny_selector"
android:visibility="gone" />
<RelativeLayout
android:id="@+id/layout_top"
android:layout_width="match_parent"
android:layout_height="60dp"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:background="@drawable/jz_title_bg"
android:gravity="center_vertical">
<ImageView
android:id="@+id/back"
android:layout_width="23dp"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_alignParentLeft="true"
android:layout_alignParentTop="true"
android:paddingStart="12dp"
android:paddingLeft="12dp"
android:scaleType="centerInside"
android:src="@drawable/jz_click_back_selector" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerVertical="true"
android:layout_marginStart="12dp"
android:layout_marginLeft="12dp"
android:layout_marginEnd="12dp"
android:layout_marginRight="12dp"
android:layout_toLeftOf="@+id/battery_time_layout"
android:layout_toEndOf="@+id/back"
android:layout_toRightOf="@+id/back"
android:ellipsize="end"
android:maxLines="2"
android:textColor="#ffffff"
android:textSize="18sp" />
<LinearLayout
android:id="@+id/battery_time_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:layout_centerVertical="true"
android:layout_marginEnd="14dp"
android:layout_marginRight="14dp"
android:gravity="center_vertical"
android:orientation="vertical">
<ImageView
android:id="@+id/battery_level"
android:layout_width="23dp"
android:layout_height="10dp"
android:layout_gravity="center_horizontal"
android:background="@drawable/jz_battery_level_10" />
<TextView
android:id="@+id/video_current_time"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_horizontal"
android:gravity="center_vertical"
android:maxLines="1"
android:textColor="#ffffffff"
android:textSize="12.0sp" />
</LinearLayout>
</RelativeLayout>
<ProgressBar
android:id="@+id/loading"
android:layout_width="@dimen/jz_start_button_w_h_normal"
android:layout_height="@dimen/jz_start_button_w_h_normal"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:indeterminateDrawable="@drawable/jz_loading"
android:visibility="invisible" />
<LinearLayout
android:id="@+id/start_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:layout_gravity="center_vertical">
<ImageView
android:id="@+id/start"
android:layout_width="@dimen/jz_start_button_w_h_normal"
android:layout_height="@dimen/jz_start_button_w_h_normal"
android:src="@drawable/jz_click_play_selector" />
</LinearLayout>
<TextView
android:id="@+id/replay_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@+id/start_layout"
android:layout_centerHorizontal="true"
android:layout_marginTop="6dp"
android:text="@string/replay"
android:textColor="#ffffff"
android:textSize="12sp"
android:visibility="invisible" />
<LinearLayout
android:id="@+id/retry_layout"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_centerVertical="true"
android:gravity="center_horizontal"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/video_loading_failed"
android:textColor="@android:color/white"
android:textSize="14sp" />
<TextView
android:id="@+id/retry_btn"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="15dp"
android:background="@drawable/jz_retry"
android:paddingLeft="9dp"
android:paddingTop="4dp"
android:paddingRight="9dp"
android:paddingBottom="4dp"
android:text="@string/click_to_restart"
android:textColor="@android:color/white"
android:textSize="14sp" />
</LinearLayout>
</RelativeLayout>
4. 使用
public class MachineActivity extends BaseActivity{
@BindView(R.id.title_bar)
TitleBar mTitleBar;
@BindView(R.id.jz_mp3)
JzvdStdMp3 mStdMp3;
@Override
protected int getLayoutId() {
return R.layout.activity_machine;
}
@Override
protected void initView() {
mTitleBar.setOnTitleBarListener(new OnTitleBarListener() {
@Override
public void onLeftClick(View view) {
finish();
}
@Override
public void onTitleClick(View view) {
}
@Override
public void onRightClick(View view) {
}
});
mStdMp3.setUp("machine.mp3", "机震测试", JzvdStd.SCREEN_NORMAL,JZMediaSystemAssertFolder.class);
mStdMp3.posterImageView.setImageResource(R.mipmap.ic_machine_thump);
}
@Override
public void onBackPressed() {
if (Jzvd.backPress()) {
return;
}
super.onBackPressed();
}
@Override
protected void onPause() {
super.onPause();
JZUtils.clearSavedProgress(this, null);
Jzvd.releaseAllVideos();
}
}