最近在做 m3u8网络视频播放,踩了不少坑,也试了不少的 框架,特别记录一下其中用的比较多的三种
media:ijkplayer 是由 bilbil 提供的开源的视频 框架,但是由过之后感觉不太好用:
优点:
1、支持 Android 和 IOS
2、支持多种视频的硬解码
缺点:
1、加载时间过长;从开始加载 到 开始播放 第一帧视频,中间最少需要十秒时间(一开始以为是自己的配置有问题,但在网上找了一下,但都没有找到好的解决方案)
2、不支持实时视频截图(由于项目的需要,需要关闭时,最后显示的视频截图,但看了一下源码,并没有找到提供相关源码)
如果有哪位大神解决的,麻烦给留个言!!!!!
media:ijkplayer 的github地址:https://github.com/bilibili/ijkplayer
集成方式:
// required, enough for most devices.
implementation 'tv.danmaku.ijk.media:ijkplayer-java:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-armv7a:0.8.8'
// Other ABIs: optional
implementation 'tv.danmaku.ijk.media:ijkplayer-armv5:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-arm64:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-x86:0.8.8'
implementation 'tv.danmaku.ijk.media:ijkplayer-x86_64:0.8.8'
// ExoPlayer as IMediaPlayer: optional, experimental
implementation 'tv.danmaku.ijk.media:ijkplayer-exo:0.8.8'
混淆配置:
-keep class tv.danmaku.ijk.media.player.** { *; }
使用:
1):IjkVideoView
public class IjkVideoView extends FrameLayout {
private Context mContext;//上下文
private IMediaPlayer mMediaPlayer = null;//视频控制类
private VideoPlayerListener mVideoPlayerListener;//自定义监听器
private SurfaceView mSurfaceView;//播放视图
private String mPath = "";//视频文件地址
public IjkVideoView(@NonNull Context context) {
super(context);
initVideoView(context);
}
public IjkVideoView(@NonNull Context context, @Nullable AttributeSet attrs) {
super(context, attrs);
initVideoView(context);
}
public IjkVideoView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
initVideoView(context);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP)
public IjkVideoView(@NonNull Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
}
public abstract static class VideoPlayerListener implements IMediaPlayer.OnPreparedListener,
IMediaPlayer.OnCompletionListener,
IMediaPlayer.OnErrorListener {
}
private void initVideoView(Context context) {
mContext = context;
setFocusable(true);
}
public void setPath(String path) {
if (TextUtils.equals("", mPath)) {
mPath = path;
initSurfaceView();
} else {
mPath = path;
loadVideo();
}
}
private void initSurfaceView() {
mSurfaceView = new SurfaceView(mContext);
mSurfaceView.getHolder().addCallback(new LmnSurfaceCallback());
LayoutParams layoutParams = new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT, Gravity.CENTER);
mSurfaceView.setLayoutParams(layoutParams);
this.addView(mSurfaceView);
}
//surfaceView的监听器
private class LmnSurfaceCallback implements SurfaceHolder.Callback {
@Override
public void surfaceCreated(SurfaceHolder holder) {
}
@Override
public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
loadVideo();
}
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
}
}
//加载视频
private void loadVideo() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
mMediaPlayer.release();
}
IjkMediaPlayer ijkMediaPlayer = new IjkMediaPlayer();
mMediaPlayer = ijkMediaPlayer;
if (mVideoPlayerListener != null) {
mMediaPlayer.setOnPreparedListener(mVideoPlayerListener);
mMediaPlayer.setOnErrorListener(mVideoPlayerListener);
}
try {
mMediaPlayer.setDataSource(mPath);
} catch (IOException e) {
e.printStackTrace();
}
mMediaPlayer.setDisplay(mSurfaceView.getHolder());
mMediaPlayer.prepareAsync();
}
public void setListener(VideoPlayerListener listener) {
this.mVideoPlayerListener = listener;
if (mMediaPlayer != null) {
mMediaPlayer.setOnPreparedListener(listener);
}
}
public boolean isPlaying() {
if (mMediaPlayer != null) {
return mMediaPlayer.isPlaying();
}
return false;
}
public void start() {
if (mMediaPlayer != null) {
mMediaPlayer.start();
}
}
public void pause() {
if (mMediaPlayer != null) {
mMediaPlayer.pause();
}
}
public void stop() {
if (mMediaPlayer != null) {
mMediaPlayer.stop();
}
}
public void reset() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
}
}
public void release() {
if (mMediaPlayer != null) {
mMediaPlayer.reset();
mMediaPlayer.release();
mMediaPlayer = null;
}
}
}
2):布局
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:background="@color/color_000000"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_centerInParent="true"
android:layout_height="wrap_content"
android:background="@color/color_000000"
android:orientation="vertical">
<com.tzyun.pip.view.IjkVideoView
android:id="@+id/jkVideoView"
android:layout_width="match_parent"
android:layout_height="300dp" />
</LinearLayout>
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:contentDescription="@null"
android:paddingStart="13dp"
android:paddingEnd="13dp"
android:src="@mipmap/back_video" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
3):Activity
public class IjkPlayerVideoActivity extends Activity {
private AlertDialog alertDialog;
private String path;
private IjkVideoView ijkVideoView;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
path = getIntent().getStringExtra("ADDRESS");
setContentView(R.layout.ijkplayervideo_layout);
findViewById(R.id.back).setOnClickListener(v -> onBackPressed());
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("播放错误");
builder.setNegativeButton("重试", (dialogInterface, i) -> setLiveParam());
builder.setPositiveButton("退出", (dialogInterface, i) -> finish());
alertDialog = builder.create();
alertDialog.setCancelable(false);
alertDialog.setCanceledOnTouchOutside(false);
setLiveParam();
}
private void setLiveParam() {
findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
IjkMediaPlayer.loadLibrariesOnce(null);
IjkMediaPlayer.native_profileBegin("libijkplayer.so");
//监听
ijkVideoView=findViewById(R.id.jkVideoView);
ijkVideoView.setListener(new IjkVideoView.VideoPlayerListener() {
@Override
public void onPrepared(IMediaPlayer mp) {
//播放成功处理
mp.start();
findViewById(R.id.progressBar).setVisibility(View.INVISIBLE);
}
@Override
public void onCompletion(IMediaPlayer iMediaPlayer) {
}
@Override
public boolean onError(IMediaPlayer mp, int what, int extra) {
ToastUtils.showToast("播放失败");
finish();
return true;
}
});
// path = "你自己的播放地址";
//路径
ijkVideoView.setPath(path);
ijkVideoView.start();
}
@Override
public void onConfigurationChanged(@NonNull Configuration newConfig) {
super.onConfigurationChanged(newConfig);
LinearLayout.LayoutParams params = (LinearLayout.LayoutParams) ijkVideoView.getLayoutParams();
//横屏
if (newConfig.orientation == Configuration.ORIENTATION_LANDSCAPE) {
params.height = RelativeLayout.LayoutParams.MATCH_PARENT;
} else {
//竖屏
params.height = (int) UnitUtils.dp2px(this, 300);
}
ijkVideoView.setLayoutParams(params);
}
@Override
protected void onDestroy() {
IjkMediaPlayer.native_profileEnd();
if (alertDialog != null && alertDialog.isShowing()) {
alertDialog.dismiss();
}
ijkVideoView.stop();
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
if (!ijkVideoView.isPlaying()) {
ijkVideoView.start();
}
}
@Override
protected void onPause() {
super.onPause();
ijkVideoView.pause();
}
}
Vitamio是适用于Android和iOS的开放式多媒体框架,具有完整,真实的硬件加速解码器和渲染器。
新的功能:
Vitamio 感觉是强大的,播放速度也很快,也可以截图,但在使用过程也遇到了问题:
根据文档的提示,Vitamio 是可以 支持 https 的,但使用的结果是 https 无法播放,所以放弃了使用,也不知道是不是因为安全证书出了问题,还有一个问题就是,Vitamio 最早是更新于七年前,很多方法已经过时了,不知道还有没有人在维护。
Vitamio gitHub地址:https://github.com/yixia/VitamioBundleStudio
使用:
1)、解压文件,将其中的vitamio导入到as中
其中的vitamio-sample是官方提供的demo,而我们要导入as的是vitamio.
2)、打开AS,File -> New -> Import Moudle,选择刚才解压文件夹下的 vitamio 文件.
导入后的文件目录中会多出vitamin文件夹,如下图
3)、在 dependencies 中添加 compile project(’:vitamio’) 如果你导入module中更改过名字的话 要改成修改后的名字 如图:
混淆配置:
# For Vitamio classes
-keep class io.vov.utils.** { *; }
-keep class io.vov.vitamio.** { *; }
4)、打开app/src/main目录下的AndroidManifest.xml,注册io.vov.vitamio.activity.InitActivity
<activity
android:name="io.vov.vitamio.activity.InitActivity"
android:configChanges="orientation|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:launchMode="singleTop"
android:theme="@android:style/Theme.NoTitleBar"
android:windowSoftInputMode="stateAlwaysHidden" />
注意:这个InitActivity存在于vitamio/src/对应的目录下,不需要用户编写.
添加权限:
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
至此,vitamio导入完毕.
5)、VideoView控件的使用
<?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">
<io.vov.vitamio.widget.CenterLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/color_000000"
android:orientation="vertical">
<io.vov.vitamio.widget.VideoView
android:id="@+id/buffer"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center" />
</io.vov.vitamio.widget.CenterLayout>
<ImageView
android:id="@+id/back"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:contentDescription="@null"
android:paddingStart="13dp"
android:paddingEnd="13dp"
android:src="@mipmap/back_video" />
<ProgressBar
android:id="@+id/progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerInParent="true" />
</RelativeLayout>
public class VideoActivity extends Activity implements MediaPlayer.OnInfoListener {
private VideoView mVideoView;
private AlertDialog alertDialog;
private int errorCount;
private int requestCode;
private String shotsName;
private String path;
private MediaPlayer mediaPlayer;
@Override
public void onCreate(Bundle icicle) {
super.onCreate(icicle);
Vitamio.isInitialized(this);
requestWindowFeature(Window.FEATURE_NO_TITLE);
getWindow().setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN);
setContentView(R.layout.vitamio_layout);
mVideoView = findViewById(R.id.buffer);
findViewById(R.id.back).setOnClickListener(v -> onBackPressed());
AlertDialog.Builder builder = new AlertDialog.Builder(this);
builder.setTitle("播放错误");
builder.setNegativeButton("重试", (dialogInterface, i) -> {
setLiveParam();
mVideoView.resume();
});
builder.setPositiveButton("退出", (dialogInterface, i) -> finish());
alertDialog = builder.create();
alertDialog.setCancelable(false);
alertDialog.setCanceledOnTouchOutside(false);
setLiveParam();
}
private void setLiveParam() {
errorCount = 0;
findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
Intent intent = getIntent();
path = intent.getStringExtra("ADDRESS");
requestCode = intent.getIntExtra("requestCode", 200);
shotsName = intent.getStringExtra("shotsName");
Uri uri = Uri.parse(path);
mVideoView.setVideoURI(uri);
mVideoView.setBufferSize(1024);
mVideoView.setHardwareDecoder(true);
mVideoView.requestFocus();
mVideoView.setOnInfoListener(this);
mVideoView.setOnPreparedListener(mediaPlayer -> {
// optional need Vitamio 4.0
mediaPlayer.setPlaybackSpeed(1.0f);
});
mVideoView.setOnErrorListener((mediaPlayer, i, i1) -> {
errorCount++;
if (errorCount > 3) {
String errMsg = "MEDIA_ERROR_UNKNOWN";
switch (i) {
case MediaPlayer.MEDIA_ERROR_UNKNOWN:
errMsg = "MEDIA_ERROR_UNKNOWN";
break;
case MediaPlayer.MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK:
errMsg = "MEDIA_ERROR_NOT_VALID_FOR_PROGRESSIVE_PLAYBACK";
break;
case MediaPlayer.MEDIA_ERROR_IO:
errMsg = "MEDIA_ERROR_IO";
break;
case MediaPlayer.MEDIA_ERROR_MALFORMED:
errMsg = "MEDIA_ERROR_MALFORMED";
break;
case MediaPlayer.MEDIA_ERROR_UNSUPPORTED:
errMsg = "MEDIA_ERROR_UNSUPPORTED";
break;
case MediaPlayer.MEDIA_ERROR_TIMED_OUT:
errMsg = "MEDIA_ERROR_TIMED_OUT";
break;
}
alertDialog.setTitle("播放错误:" + errMsg);
alertDialog.show();
} else {
new Handler().postDelayed(this::setLiveParam, 1000);
}
return true;
});
}
@Override
protected void onDestroy() {
if (alertDialog != null && alertDialog.isShowing())
alertDialog.dismiss();
super.onDestroy();
}
@Override
protected void onResume() {
super.onResume();
findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
}
@Override
public boolean onInfo(MediaPlayer mp, int what, int extra) {
mediaPlayer = mp;
switch (what) {
case MediaPlayer.MEDIA_INFO_BUFFERING_START:
if (mVideoView.isPlaying()) {
mVideoView.pause();
}
findViewById(R.id.progressBar).setVisibility(View.VISIBLE);
break;
case MediaPlayer.MEDIA_INFO_BUFFERING_END:
mVideoView.start();
findViewById(R.id.progressBar).setVisibility(View.INVISIBLE);
break;
case MediaPlayer.MEDIA_INFO_DOWNLOAD_RATE_CHANGED:
LogUtils.Log(getClass().getSimpleName(), "vitamio download rate" + extra + "kb/s");
break;
}
return true;
}
// 捕获返回键的方法2
@Override
public void onBackPressed() {
if (requestCode == 200) {
super.onBackPressed();
} else {
viewShot();
}
}
/**
* view截图
*
* @return
*/
public void viewShot() {
if (mediaPlayer != null) {
// 核心代码start
Bitmap bitmap = mediaPlayer.getCurrentFrame();
String savePath;
if (bitmap != null) {
String path = getExternalFilesDir(null).toString() + "/monitoringScreenshots/";
File file = new File(path);
if (!file.exists()) {
file.mkdirs();
}
try {
path = path + shotsName + ".png";
File file2 = new File(path);
if (file2.exists()) {
file2.delete();
}
savePath = BitmapUtils.saveBitmapToFileWithFilePathWithOriginParams(path, bitmap).toString();
setResult(savePath);
} catch (IOException e) {
e.printStackTrace();
setResult("");
}
} else {
setResult("");
}
} else {
setResult("");
}
}
/**
* savePath 截图保存地址
*
* @param savePath
*/
private void setResult(String savePath) {
Intent intent = new Intent();
intent.putExtra("screenshotPath", savePath);
setResult(RESULT_OK, intent);
finish();
}
}
VideoView常用函数
/**
* 获取扫描视频的Uri。
* 参数layout(缩放参数)参见MediaPlayer的常量:VIDEO_LAYOUT_ORIGIN(原始大小)、VIDEO_LAYOUT_SCALE(画面全屏)、VIDEO_LAYOUT_STRETCH(画面拉伸)、VIDEO_LAYOUT_ZOOM(画面裁剪)、VIDEO_LAYOUT_FIT_PARENT(画面铺满)
* 参数aspectRation(宽高比),为0将自动检测
*/
public void setVideoLayout(int layout,float aspectRatio);
//Surface是否有效。 参见Surface的isValid方法。
public boolean isValid();
//设置视频路径。
public void setVideoPath(String path);
//设置视频URI。(可以是网络视频地址)
public void setVideoURI(Uri uri);
//停止视频播放,并释放资源。
public void stopPlayback();
/**
* 设置媒体控制器。
* 参数controller:媒体控制器,注意是io.vov.vitamio.widget.MediaController。
*/
public void setMediaController(MediaController controller);
//注册一个回调函数,在视频预处理完成后调用。在视频预处理完成后被调用。此时视频的宽度、高度、宽高比信息已经获取到,此时可调用seekTo让视频从指定位置开始播放。
public void setOnPreparedListener(OnPreparedListener l);
//获取当前播放位置。
public long getCurrentPosition();
//设置播放位置。单位毫秒
public void seekTo(long msec);
//是否正在播放。
public boolean isPlaying();
//获取缓冲百分比。
public int getBufferPercentage();
/**
* 设置视频质量。
* 参数quality参见MediaPlayer的常量:VIDEOQUALITY_LOW(流畅)、VIDEOQUALITY_MEDIUM(普通)、VIDEOQUALITY_HIGH(高质)
*/
public void setVideoQuality(int quality);
//设置视频缓冲大小。默认1024KB,单位byte
public void setBufferSize(int bufSize);
//检测是否缓冲完毕。
public boolean isBuffering();
//设置元数据编码。例如:UTF-8
public void setMetaEncoding(String encoding);
ExoPlayer是构建在Android低水平媒体API之上的一个应用层媒体播放器。和Android内置的媒体播放器相比,ExoPlayer有许多优点。ExoPlayer支持内置的媒体播放器支持的所有格式外加自适应格式DASH和SmoothStreaming。ExoPlayer可以被高度定制和扩展以适应不同的使用场景。
优点:
缺点:
ExoPlayer gitHub 地址:https://github.com/google/ExoPlayer/
集成方式:
implementation 'com.google.android.exoplayer:exoplayer:2.13.2'
使用:
class ExoplayerActivity : Activity(), Player.EventListener {
var path: String? = null
private var simpleExoPlayer: SimpleExoPlayer? = null
private var requestCode = 0
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
requestWindowFeature(Window.FEATURE_NO_TITLE)
window.setFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN, WindowManager.LayoutParams.FLAG_FULLSCREEN)
setContentView(R.layout.activity_exoplayer)
path = intent.getStringExtra("ADDRESS")
requestCode = intent.getIntExtra("requestCode", 200)
initView()
}
private fun initView() {
simpleExoPlayer = SimpleExoPlayer.Builder(this).build()
path?.let {
val mediaItem: MediaItem = MediaItem.fromUri(it)
simpleExoPlayer?.setMediaItem(mediaItem)
simpleExoPlayer?.prepare()
}
simpleExoPlayer?.addListener(this)
exoplayer_videoView.player = simpleExoPlayer
findViewById<View>(R.id.exoplayer_back).setOnClickListener { onBackPressed() }
}
override fun onPlaybackStateChanged(state: Int) {
// Player.STATE_IDLE -> "ExoPlayer.闲置状态 -"
// Player.STATE_BUFFERING -> "ExoPlayer.国家缓冲 -"
// Player.STATE_READY -> "ExoPlayer.准备就绪 -"
// Player.STATE_ENDED -> "ExoPlayer.状态已结束 -"
// else -> "UNKNOWN_STATE
when (state) {
Player.STATE_READY -> {
exoplayer_progressBar.visibility = View.GONE
}
else -> {
}
}
}
override fun onResume() {
super.onResume()
simpleExoPlayer?.play()
}
override fun onStop() {
super.onStop()
simpleExoPlayer?.pause()
}
override fun onDestroy() {
super.onDestroy()
simpleExoPlayer?.removeListener(this)
simpleExoPlayer?.release()
}
// 捕获返回键的方法2
@Override
override fun onBackPressed() {
if (requestCode == 200) {
super.onBackPressed()
} else {
saveBitmap()
}
}
/**
* 获取并保存截图
*/
private fun saveBitmap() {
try {
val textureView: TextureView = exoplayer_videoView.videoSurfaceView as TextureView
val screenshotBitmap = textureView.bitmap
val path = getExternalFilesDir(null).toString() + "/monitoringScreenshots/"
val file = File(path)
if (!file.exists()) {
file.mkdirs()
}
//文件名为时间
val timeStamp = System.currentTimeMillis()
val sdf = SimpleDateFormat("yyyyMMdd_HHmmss")
val sd = sdf.format(Date(timeStamp))
val fileName = "$sd.jpg"
val savePath = path + fileName
val outputStream = FileOutputStream(savePath)
screenshotBitmap.compress(Bitmap.CompressFormat.JPEG, 100, outputStream)
outputStream.flush()
outputStream.close()
setResult(savePath)
} catch (e: IOException) {
e.printStackTrace()
finish()
}
}
/**
* savePath 截图保存地址
*
* @param savePath
*/
private fun setResult(savePath: String) {
val intent = Intent()
intent.putExtra("screenshotPath", savePath)
setResult(RESULT_OK, intent)
finish()
}
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout 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/color_000000"
tools:context="com.tzyun.pip.ijkPlayer.ExoplayerActivity">
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/exoplayer_videoView"
android:layout_width="0dp"
android:layout_height="0dp"
app:auto_show="false"
app:use_controller="false"
app:surface_type="texture_view"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/exoplayer_back"
android:layout_width="wrap_content"
android:layout_height="48dp"
android:contentDescription="@null"
android:paddingStart="13dp"
android:paddingEnd="13dp"
android:src="@mipmap/back_video"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<ProgressBar
android:id="@+id/exoplayer_progressBar"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>