最近简单学了下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.视频状态是播放中要禁用手机自动锁屏,不然看着看着手机就熄屏了。我的是虽然不会自动锁屏了,但是亮度还是会降低。
总之,写的很粗略,希望大佬能指正。