最近简单学了下ExoPlayer,做了一个简单的影视播放demo。
一般的视频资源,网上有一些免费的测试接口,想要的话可以找一下。
实现结果如下:
我这里是实现了简单的全屏播放、倍速播放、左右屏幕拖动进度时间、上一集和下一集。
布局文件:
<?xml version="1.0" encoding="utf-8"?><LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_weight="3">
<FrameLayout
android:id="@+id/player_room"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:background="@color/black">
<!-- 视频-->
<com.google.android.exoplayer2.ui.PlayerView
android:id="@+id/player_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:controller_layout_id="@layout/exoplayer_mview"/>
<!-- 拖拉进度时间-->
<TextView
android:id="@+id/slow_time"
android:textColor="@color/pink"
android:textSize="20dp"
android:visibility="gone"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/loading_tip"
android:text="加载中..."
android:textColor="@color/white"
android:visibility="gone"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:id="@+id/double_speed_tip"
android:text="倍速播放中"
android:textColor="@color/white"
android:visibility="gone"
android:layout_gravity="center"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</FrameLayout>
<LinearLayout
android:id="@+id/video_introduce"
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_margin="10dp"
android:layout_weight="2"
android:orientation="vertical">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp"
android:layout_marginBottom="5dp"
android:text="简介"
android:textSize="20dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/item_title"
android:layout_marginLeft="5dp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:textSize="20dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="5dp">
<TextView
android:id="@+id/item_releaseTime"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="/"/>
<TextView
android:id="@+id/item_region"
android:layout_width="wrap_content"
android:layout_height="wrap_content"/>
</LinearLayout>
<TextView
android:id="@+id/item_introduce"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="5dp"/>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="5dp"
android:text="剧集"
android:textSize="20dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/playList_sum"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
<!-- 剧集-->
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/video_episodes"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"/>
</LinearLayout>
</LinearLayout>
playerView我是自定义了UI: app:controller_layout_id="@layout/exoplayer_mview"
exoplayer_mview布局:
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="bottom"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<TextView
android:id="@+id/exo_position"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="10dp"
android:layout_marginEnd="10dp"
android:text="00:00"
android:textColor="@android:color/white" />
<com.google.android.exoplayer2.ui.DefaultTimeBar
android:id="@id/exo_progress"
android:layout_width="0dp"
android:layout_height="20dp"
android:layout_weight="1" />
<TextView
android:id="@+id/exo_duration"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="10dp"
android:layout_marginStart="10dp"
android:text="00:00"
android:textColor="@android:color/white" />
</LinearLayout>
<RelativeLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<ImageView
android:id="@+id/exo_m_prev"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginLeft="100dp"
android:layout_alignParentLeft="true"
android:src="@drawable/pre_btn" />
<ImageView
android:id="@+id/exo_play"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerHorizontal="true"
android:src="@drawable/play_btn" />
<ImageView
android:id="@+id/exo_pause"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_centerHorizontal="true"
android:src="@drawable/pause_btn" />
<ImageView
android:id="@+id/exo_m_next"
android:layout_width="20dp"
android:layout_height="20dp"
android:layout_marginRight="100dp"
android:layout_alignParentRight="true"
android:src="@drawable/next_btn" />
<ImageView
android:id="@+id/exo_full"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginLeft="30dp"
android:layout_alignParentRight="true"
android:src="@drawable/full" />
</RelativeLayout>
</LinearLayout>
</FrameLayout>
activity:
package com.example.classcard;import static androidx.constraintlayout.helper.widget.MotionEffect.TAG;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.PowerManager;
import android.provider.Settings;
import android.util.Log;
import android.util.TypedValue;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.FrameLayout;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.RelativeLayout;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.appcompat.app.AppCompatActivity;
import androidx.core.content.ContextCompat;
import androidx.core.view.GestureDetectorCompat;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import com.example.classcard.api.Mapi;
import com.example.classcard.api.Result_play;
import com.example.classcard.fragment.MovieFragment;
import com.example.classcard.pojo.Play;
import com.example.classcard.pojo.PlayItem;
import com.example.classcard.pojo.ReVideo;
import com.google.android.exoplayer2.PlaybackParameters;
import com.google.android.exoplayer2.Player;
import com.google.android.exoplayer2.SimpleExoPlayer;
import com.google.android.exoplayer2.source.MediaSource;
import com.google.android.exoplayer2.source.ProgressiveMediaSource;
import com.google.android.exoplayer2.source.hls.HlsMediaSource;
import com.google.android.exoplayer2.ui.AspectRatioFrameLayout;
import com.google.android.exoplayer2.ui.DefaultTimeBar;
import com.google.android.exoplayer2.ui.PlayerControlView;
import com.google.android.exoplayer2.ui.PlayerView;
import com.google.android.exoplayer2.upstream.DataSource;
import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
import java.util.ArrayList;
import java.util.List;
import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;
import okhttp3.OkHttpClient;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
import retrofit2.Retrofit;
import retrofit2.converter.gson.GsonConverterFactory;
public class PlayVideoActivity extends AppCompatActivity {
private TextView textView_title;
private TextView textView_region;
private TextView textView_releaseTime;
private TextView textView_introduce;
private TextView textView_playListSum;
private PlayerView playerView;
private RecyclerView recyclerView;
private String videoId;
private List<PlayItem> playList = new ArrayList<>();
private LinearLayout videoIntroduce;
private SimpleExoPlayer player;
private boolean isFullscreen = false; // 记录当前是否为全屏状态
private long playbackPosition = 0;
private int selectedPosition = 0;
private String videoTitle;
private int flag = 0;
private TextView textView;
private ReVideo reVideo;
private EpisodeAdapter adapter;
private PowerManager.WakeLock wakeLock;
private TextView doubleSpeedTip;
private TextView loadingTip;
private TextView slowTimeTip;
private GestureDetectorCompat gestureDetector;
private Boolean isPlay = false;
private boolean isSpeeding = false;//记录是否在加速中
private long slowPosition;
private int slow_flag = 0;//记录是否在滑动屏幕
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.play_video);
textView_title = findViewById(R.id.item_title);
textView_region = findViewById(R.id.item_region);
textView_releaseTime = findViewById(R.id.item_releaseTime);
textView_introduce = findViewById(R.id.item_introduce);
textView_playListSum = findViewById(R.id.playList_sum);
playerView = findViewById(R.id.player_view);
videoIntroduce = findViewById(R.id.video_introduce);
recyclerView = findViewById(R.id.video_episodes);
doubleSpeedTip = findViewById(R.id.double_speed_tip);
loadingTip = findViewById(R.id.loading_tip);
slowTimeTip = findViewById(R.id.slow_time);
reVideo = (ReVideo) getIntent().getSerializableExtra("videoItem");
textView_title.setText(reVideo.getTitle());
textView_region.setText(reVideo.getRegion());
textView_releaseTime.setText(reVideo.getReleaseTime());
textView_introduce.setText(reVideo.getDescs());
videoId = reVideo.getVideoId();
if (savedInstanceState != null) {
isFullscreen = savedInstanceState.getBoolean("isFullscreen");
playbackPosition = savedInstanceState.getLong("playbackPosition");
selectedPosition = savedInstanceState.getInt("selectedPosition");
}
try {
getData(videoId);
} catch (Exception e) {
e.printStackTrace();
}
}
//视频播放
private void playVideo(String url){
player = new SimpleExoPlayer.Builder(getBaseContext()).build();
Uri uri = Uri.parse(url);
DataSource.Factory dataSourceFactory = new DefaultHttpDataSourceFactory();
MediaSource mediaSource = new HlsMediaSource.Factory(dataSourceFactory)
.createMediaSource(uri);
playerView.setPlayer(player);
player.prepare(mediaSource);
player.setPlayWhenReady(true);
player.seekTo(playbackPosition);
// 找到控制组件
PlayerControlView playerControlView = playerView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_controller);
// 创建 TextView 组件
if(flag==0){
textView = new TextView(this);
videoTitle = reVideo.getTitle();
textView.setText(videoTitle+playList.get(selectedPosition).getTitle());
textView.setTextColor(ContextCompat.getColor(this, R.color.white));
// 设置 TextView 的位置和大小
FrameLayout.LayoutParams params2 = new FrameLayout.LayoutParams(
FrameLayout.LayoutParams.WRAP_CONTENT,
(int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics())
);
params2.gravity = Gravity.TOP | Gravity.LEFT;
params2.setMargins(20,20,0,0);
textView.setLayoutParams(params2);
playerControlView.addView(textView,params2);
flag =1;
}
//设置全屏按钮
ImageView fullscreenButton = playerView.findViewById(R.id.exo_full);
fullscreenButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
toggleFullscreen();
}
});
//上一集和下一集按钮
ImageView nextButton = playerView.findViewById(R.id.exo_m_next);
ImageView prevButton = playerView.findViewById(R.id.exo_m_prev);
nextButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (selectedPosition < playList.size() - 1) {
selectedPosition++; // 更新选中的视频位置到下一个
releasePlayer();
playVideo(playList.get(selectedPosition).getChapterPath());
textView.setText(videoTitle+playList.get(selectedPosition).getTitle());
adapter.notifyDataSetChanged();
} else {
Toast.makeText(getBaseContext(), "已经是最后一集了", Toast.LENGTH_SHORT).show();
}
}
});
prevButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
if (selectedPosition > 0) {
selectedPosition--; // 更新选中的视频位置到上一个
releasePlayer();
playVideo(playList.get(selectedPosition).getChapterPath());
textView.setText(videoTitle+playList.get(selectedPosition).getTitle());
adapter.notifyDataSetChanged();
} else {
Toast.makeText(getBaseContext(), "已经是第一集了", Toast.LENGTH_SHORT).show();
}
}
});
// 设置player的监听器
player.addListener(new Player.EventListener() {
@Override
public void onPlayerStateChanged(boolean playWhenReady, int playbackState) {
if (playbackState == Player.STATE_READY && playWhenReady) {
// 视频播放中
loadingTip.setVisibility(View.GONE);
isPlay = true;
// 禁用手机自动锁屏
PowerManager powerManager = (PowerManager) getSystemService(POWER_SERVICE);
wakeLock = powerManager.newWakeLock(PowerManager.FULL_WAKE_LOCK, "MyApp:MyWakeLockTag");
wakeLock.acquire();
} else if (playbackState == Player.STATE_READY) {
// 视频暂停中
isPlay = false;
loadingTip.setVisibility(View.GONE);
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
} else if (playbackState == Player.STATE_ENDED) {
// 视频播放完成
if(selectedPosition<playList.size()-1){
Toast.makeText(getBaseContext(), "自动为你播放下一集", Toast.LENGTH_SHORT).show();
selectedPosition++; // 更新选中的视频位置到下一个
releasePlayer();
playVideo(playList.get(selectedPosition).getChapterPath());
textView.setText(videoTitle+playList.get(selectedPosition).getTitle());
adapter.notifyDataSetChanged();
}else{
Toast.makeText(getBaseContext(), "已经是最后一集了", Toast.LENGTH_SHORT).show();
}
} else if (playbackState == Player.STATE_BUFFERING) {
// 视频缓冲中
isPlay = false;
doubleSpeedTip.setVisibility(View.GONE);
loadingTip.setVisibility(View.VISIBLE);
} else if (playbackState == Player.STATE_IDLE) {
// 视频空闲状态
// ...
}
}
});
//设置长按倍速播放
playerView.setOnTouchListener(new View.OnTouchListener() {
private Handler handler = new Handler();
private Runnable showTextRunnable = new Runnable() {
@Override
public void run() {
if (isSpeeding) {
player.setPlaybackParameters(new PlaybackParameters(2.0f));
doubleSpeedTip.setVisibility(View.VISIBLE);
}
}
};
@Override
public boolean onTouch(View v, MotionEvent event) {
switch (event.getAction()) {
case MotionEvent.ACTION_DOWN:
// 按下时调整播放速度为2倍
if(isPlay){
isSpeeding = true;
handler.postDelayed(showTextRunnable, 800);
}
break;
case MotionEvent.ACTION_UP:
slowTimeTip.setVisibility(View.GONE);
if(slow_flag == 1){
player.seekTo(slowPosition);
slow_flag = 0;
}
case MotionEvent.ACTION_CANCEL:
// 松开手时恢复正常播放速度
isSpeeding = false;
player.setPlaybackParameters(new PlaybackParameters(1.0f));
doubleSpeedTip.setVisibility(View.GONE);
handler.removeCallbacks(showTextRunnable);
break;
}
return gestureDetector.onTouchEvent(event);
}
});
//拖动进度
DefaultTimeBar timeBar = playerView.findViewById(com.google.android.exoplayer2.ui.R.id.exo_progress);
gestureDetector = new GestureDetectorCompat(this, new GestureDetector.SimpleOnGestureListener() {
private long duration;
private static final float SLOW_FACTOR = 0.01f; // 缓慢播放速度的因子
@Override
public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
// 获取视频的总时长
if(slow_flag==0){
duration = player.getDuration();
slowPosition = player.getCurrentPosition();
slow_flag = 1;
}
// 如果总时长大于0且手势滑动距离不为0
if (duration > 0) {
isSpeeding = false;
doubleSpeedTip.setVisibility(View.GONE);
if(distanceX>0){
slowPosition = Math.max(0, slowPosition - 1000);
}else if(distanceX<0){
slowPosition = Math.min(duration, slowPosition + 1000);
}
slowTimeTip.setText(formatTime(slowPosition) + "/" + formatTime(duration));
slowTimeTip.setVisibility(View.VISIBLE);
return true;
}
return false;
}
});
}
public String formatTime(long milliseconds) {
long seconds = (milliseconds / 1000) % 60;
long minutes = (milliseconds / (1000 * 60)) % 60;
long hours = (milliseconds / (1000 * 60 * 60)) % 24;
String timeString = String.format("%02d:%02d:%02d", hours, minutes, seconds);
return timeString;
}
//一些生命周期
@Override
protected void onRestart() {
super.onRestart();
releasePlayer();
playVideo(playList.get(selectedPosition).getChapterPath());
player.setPlayWhenReady(false);
}
@Override
protected void onPause() {
super.onPause();
if(player != null){
playbackPosition = player.getCurrentPosition();
}
}
@Override
protected void onStop() {
super.onStop();
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
// 保存播放进度和播放状态
if(player != null){
playbackPosition = player.getCurrentPosition();
}
releasePlayer();
}
//保存activity信息
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBoolean("isFullscreen", isFullscreen);
outState.putLong("playbackPosition", playbackPosition);
outState.putInt("selectedPosition", selectedPosition);
}
// 在此处实现全屏功能
private void toggleFullscreen() {
if (isFullscreen) {
// 退出全屏
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT);
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FIT);
videoIntroduce.setVisibility(View.VISIBLE);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE);
getWindow().addFlags(WindowManager.LayoutParams.FLAG_FULLSCREEN);
playerView.setResizeMode(AspectRatioFrameLayout.RESIZE_MODE_FILL);//视频填充
videoIntroduce.setVisibility(View.GONE);
}
isFullscreen = !isFullscreen;
}
//释放视频资源
private void releasePlayer() {
if (player != null) {
player.release();
player = null;
}
}
//剧集列表
public class EpisodeAdapter extends RecyclerView.Adapter<EpisodeAdapter.ViewHolder> {
private List<PlayItem> episodes; // 剧集列表数据
public EpisodeAdapter(List<PlayItem> episodes) {
this.episodes = episodes;
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_episode, parent, false);
return new ViewHolder(view);
}
public class ViewHolder extends RecyclerView.ViewHolder {
private TextView textViewNum;
public ViewHolder(@NonNull View itemView) {
super(itemView);
textViewNum = itemView.findViewById(R.id.textView_num);
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
PlayItem episode = episodes.get(position);
holder.textViewNum.setText(episode.getTitle());
// 设置默认选中第一项
if (position == selectedPosition) {
holder.textViewNum.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.blue));
} else {
holder.textViewNum.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.black));
}
holder.itemView.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
PlayItem playItem = episodes.get(position);
releasePlayer();
playVideo(playItem.getChapterPath());
selectedPosition = position;
textView.setText(videoTitle+playList.get(position).getTitle());
// 设置选中项字体颜色为选中颜色
for (int i = 0; i < getItemCount(); i++) {
ViewHolder viewHolder = (ViewHolder) recyclerView.findViewHolderForAdapterPosition(i);
if (viewHolder != null) {
viewHolder.textViewNum.setTextColor(ContextCompat.getColor(viewHolder.itemView.getContext(), R.color.black));
}
}
holder.textViewNum.setTextColor(ContextCompat.getColor(holder.itemView.getContext(), R.color.blue));
}
});
}
@Override
public int getItemCount() {
return episodes.size();
}
}
}
以上的getData方法是我自定义的获取视频资源的。
需要注意的是:
1.要实现全屏播放,如果不另外添加设置的话,横屏的时侯activity会重新创建,从而影响视频要重新加载。
在AndroidManifest.xml文件你的播放视频的activity添加以下设置:
android:configChanges="orientation|screenSize"
比如我的是PlayVideoActivity:
<activity android:name=".PlayVideoActivity" android:configChanges="orientation|screenSize"/>
2.注意应用切到后台时在生命周期的处理,不然再从后台进入前台有影响。我是切到后台时保存视频的进度,重新进入前台后恢复进度。
3.视频状态是播放中要禁用手机自动锁屏,不然看着看着手机就熄屏了。我的是虽然不会自动锁屏了,但是亮度还是会降低。
总之,写的很粗略,希望大佬能指正。