【Android】Jetpack Media3 播放音频文件
三三要成为安卓糕手
引入
在Android也会中有各种各样的媒体资源播放器,其中包括Android SDK中的MediaPlayer、SoundPool,以及官方扩展库Jetpack Media3、ExoPlayer ,以及一些优秀的第三方库FFmpeg、DKVideoPlayer、JZVideo等等。
一:Jectpack Media3介绍
Jectpack Media3是目前安卓官方比较推荐的音视频库,他对ExpPlayer做了封装,可以让我们更方便的实现音视频的播放与控制。
1:添加依赖和网络权限
// Media3 ExoPlayer 库
implementation "androidx.media3:media3-exoplayer:1.0.0"
// 可选:用于控制播放的 UI 组件
implementation "androidx.media3:media3-ui:1.0.0"
<uses-permission android:name="android.permission.INTERNET" />android:usesCleartextTraffic="true"
2:需求
我们要做一个音乐播放器,点击对应的按钮,产生对应的效果;seekbar进度条跟随音乐的播放向右移动,左侧时间提示代表已经播放到哪个位置了,右侧时间提示代表这首音乐的总时长
3:客户端界面设计
不熟悉的知识点使用:同一行控件水平对齐,这是约束布局ConstraintLayout中很常见的“让控件在垂直方向上与另一个控件对齐” 的约束写法
app:layout_constraintBottom_toBottomOf="@id/seek_bar"
app:layout_constraintTop_toTopOf="@id/seek_bar"
代码如下
<?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:id="@+id/main"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"tools:context=".AudioActivity"><Buttonandroid:id="@+id/btn_prepare"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_marginTop="30dp"android:text="装载媒资源体"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toTopOf="parent" /><Buttonandroid:id="@+id/btn_play"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="play"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/btn_prepare" /><Buttonandroid:id="@+id/btn_pause"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="暂停"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/btn_play" /><Buttonandroid:id="@+id/btn_stop"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="停止"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/btn_pause" /><TextViewandroid:id="@+id/tv_duration"android:layout_width="wrap_content"android:layout_height="wrap_content"android:layout_marginTop="30dp"android:text="00:00:00"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@id/btn_stop" /><SeekBarandroid:id="@+id/seek_bar"android:layout_width="0dp"android:layout_height="wrap_content"app:layout_constraintBottom_toBottomOf="@+id/tv_duration"app:layout_constraintEnd_toStartOf="@+id/tv_total"app:layout_constraintStart_toEndOf="@id/tv_duration"app:layout_constraintTop_toTopOf="@+id/tv_duration" /><TextViewandroid:id="@+id/tv_total"android:layout_width="wrap_content"android:layout_height="wrap_content"android:text="00:00:00"app:layout_constraintBottom_toBottomOf="@id/seek_bar"app:layout_constraintEnd_toEndOf="parent"app:layout_constraintStart_toEndOf="@+id/seek_bar"app:layout_constraintTop_toTopOf="@id/seek_bar" /><androidx.media3.ui.PlayerViewandroid:id="@+id/player_view"android:layout_width="match_parent"android:layout_height="0dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/seek_bar"/></androidx.constraintlayout.widget.ConstraintLayout>
二:主线程代码分析
对四个按钮和SeekBar设置监听,完成媒体资源的初始化。
@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_audio);seekbar = findViewById(R.id.seek_bar);tvDuration = findViewById(R.id.tv_duration);tvtotal = findViewById(R.id.tv_total);playerView = findViewById(R.id.player_view);seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if(fromUser){player.seekTo(progress);}}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}});/*** 装载音乐*/findViewById(R.id.btn_prepare).setOnClickListener(view -> {//构建MediaItem,以使Player使用MediaItem mediaItem = MediaItem.fromUri(getNetWorkUri());player.setMediaItem(mediaItem);//装载,加载媒体player.prepare();});/*** 播放音乐*/findViewById(R.id.btn_play).setOnClickListener(view -> {if(!player.isPlaying()){player.play();}});/*** 暂停音乐*/findViewById(R.id.btn_pause).setOnClickListener(view -> {if(player.isPlaying()){player.pause();}});/*** 停止音乐*/findViewById(R.id.btn_stop).setOnClickListener(view -> {player.stop();});//初始化initPlayer();}
1:((20250909090445-jobop24 “装载音乐”))
为播放器ExoPlayer设置媒体源
- MediaItem.fromUri(((20250909090445-jobop24 “getNetWorkUri()”))); 创建媒体源
- player.setMediaItem(mediaItem);播放器和媒体源关联起来
2:ExoPlayer基础方法使用
- .prepare 装载加载媒体
- .play 开始播放
- .pause 暂停播放
- .stop 结束播放
三:initPlayer()初始化
/*** 初始化播放器*/private void initPlayer(){player = new ExoPlayer.Builder(this).build();player.addListener(new Player.Listener(){/*** 空闲、初始状态:IDLE* 播放器正在缓存中,需要加载数据:BUFFERING* 准备完毕:READY* 播放完毕:ENDED*/@Overridepublic void onPlaybackStateChanged(int playbackState) {switch (playbackState){case Player.STATE_IDLE:Log.i(TAG, "onPlaybackStateChanged: 播放器空闲");break;case Player.STATE_BUFFERING:Log.i(TAG, "onPlaybackStateChanged: 缓冲......");break;case Player.STATE_READY://获取当前装载好的媒体资源播放时长,单位是毫秒long duration = player.getDuration();seekbar.setMax((int)duration);//设置音乐总时长显示String formatTime = formatTime(duration);tvtotal.setText(formatTime);Log.i(TAG, "onPlaybackStateChanged: 播放器准备就绪");case Player.STATE_ENDED:Log.i(TAG, "onPlaybackStateChanged: 关闭播放器");break;}Log.i(TAG, "onPlaybackStateChanged: playbackState = " + playbackState);}@Overridepublic void onIsPlayingChanged(boolean isPlaying) {Log.i(TAG, "onIsPlayingChanged: isPlaying = " + isPlaying);}@Overridepublic void onPlayerError(PlaybackException error) {Log.i(TAG, "onPlayerError: error" + error);if (error.errorCode == PlaybackException.ERROR_CODE_TIMEOUT){Toast.makeText(AudioActivity.this, "加载超时", Toast.LENGTH_SHORT).show();}}});playerView.setPlayer(player);//线程之间的通信handler = new Handler();//创建一个任务runnable = new Runnable(){@Overridepublic void run() {long currentPosition = player.getCurrentPosition();seekbar.setProgress((int) currentPosition);tvDuration.setText(formatTime(currentPosition));handler.postDelayed(this,1000);}};handler.post(runnable);}
1:new ExoPlayer.Builder(this).build();
使用 ExoPlayer库创建播放器,添加监听器,重写三个监听方法
(1)onPlaybackStateChanged(int playbackState)
当ExoPlayer的播放状态发生改变的时候被调用,playbackState参数是一个整数,用于表示播放器当前所处的状态
常见状态值:
Player.STATE_IDLE
:表示播放器处于空闲状态,既没有加载媒体资源,也没有进行播放,通常是播放器刚刚创建或者播放完成后回到的状态。Player.STATE_BUFFERING
:表示播放器正在缓冲媒体数据,此时还不能进行流畅播放,正在从数据源(如网络)获取数据并填充缓冲区。Player.STATE_READY
:表示播放器已经准备好,可以进行播放了,媒体数据已经加载完成或者缓冲区已经填充到可以开始播放的程度。Player.STATE_ENDED
:表示播放已经结束,已经到达了媒体文件的末尾。
(2)onIsPlayingChanged(boolean isPlaying)
这个方法会在播放器的播放 / 暂停状态发生变化时被调用;isPlaying为true表示正在播放,反之暂停
(3)onPlayerError(PlaybackException error)
当 ExoPlayer
在播放过程中遇到错误时,这个方法会被调用;此处对加载超时错误PlaybackException.ERROR_CODE_TIMEOUT
进行的逻辑处理
2:效果
点击装载媒体资源
播放暂停
播放停止
四:getNetWorkUri()
解析网络上音乐资源,把字符串形式转化为uri形式
private Uri getNetWorkUri(){String music = "http://titok.fzqq.fun/uploads/20241007/09557aab316732bcc04e8fc3a24df8a2.mp3";Uri uri = Uri.parse(music);return uri;}
五:进度条关联播放器
实现目标:拉动SeekBar进度条,播放器音乐进度随之改变;
seekbar.setOnSeekBarChangeListener(new SeekBar.OnSeekBarChangeListener() {@Overridepublic void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {if(fromUser){player.seekTo(progress);}}@Overridepublic void onStartTrackingTouch(SeekBar seekBar) {}@Overridepublic void onStopTrackingTouch(SeekBar seekBar) {}});
SeekBar之前学习过,这里在深入了解一下onProgressChanged
(SeekBar进度变化监听器)方法,
参数2:int progress
- 表示
SeekBar
当前的进度值(整数),范围是0
到SeekBar
的最大值(setMax()
设定) - 用途:获取当前进度的具体数值,例如在音频播放场景中,可对应播放进度的毫秒数或百分比
参数3:boolean fromUser
-
表示进度变化的触发来源:
true
:用户手动操作导致的(比如用户拖动滑块)。false
:代码自动更新导致的(比如播放进度随时间自动推进)。
-
用途:区分进度变化的来源,避免逻辑冲突。例如只有当用户手动拖动,才执行 “跳转到对应进度播放” 的逻辑
1:player.seekTo(progress);
SeekBar和播放器关联
progress
参数表示目标位置,单位是毫秒 (ms); 调用后,播放器会尝试跳转到指定位置并继续播放(如果处于播放状态)
六:播放总时间显示
播放器里面是以毫秒为进度的,把进度条的总进度与当前资源做统一;
假设我们要播放的资源是1w毫秒,那么把我们当前的seekbar也要设置为1w毫秒
点击装载资源之后,就可以把总播放时长显示出来
case Player.STATE_READY://获取当前装载好的媒体资源播放时长,单位是毫秒long duration = player.getDuration();seekbar.setMax((int)duration);//设置音乐总时长显示String formatTime = formatTime(duration);tvtotal.setText(formatTime);Log.i(TAG, "onPlaybackStateChanged: 播放器准备就绪");
1:formatTime方法
传参毫秒,转化成时分秒的形式;
private String formatTime(long time){//4分钟的时长long hours = (time / (1000 * 60 * 60) ) % 24;//1min 换算成ms 有多少个1分钟 , 在取模60 就是 不足一小时又大于60秒的值long minutes = (time/(60 * 1000)) % 60;long seconds = (time / 1000) % 60;//占位符,至少是两位数,不足两位数十位补0String format = String.format("%02d:%02d:%02d", hours, minutes, seconds);return format;}
(1)String.format格式化字符串
%02d
是格式化占位符的关键部分:
%d
表示要格式化的是整数2
表示最小宽度为 2 位0
表示 “如果不足指定宽度,则在前面补 0”(而不是默认的补空格)
七:播放进度时间实时显示 & SeekBar滑块实时更新
//线程之间的通信
handler = new Handler();
//创建一个任务
runnable = new Runnable() {@Overridepublic void run() {// 1. 获取当前播放位置(毫秒数)long currentPosition = player.getCurrentPosition();// 2. 滑块实时随音乐播放进度而前进,更新 SeekBar 进度(需转为 int 类型)seekBar.setProgress((int) currentPosition);// 3. 更新当前时间文本(格式化毫秒为时分秒)tvDruation.setText(formatTime(currentPosition));// 4. 延迟 1 秒后再次执行自身(形成周期性循环)handler.postDelayed(this, 1000);}
};
handler.post(runnable);
-
Handler在主线程中创建,那么它就指代主线程,这里将 Runnable 任务切换到 Handler 所关联的线程(主线程)中执行,并实现周期性任务调度;
-
效果就是周期性的更新UI的两处地方
- 左侧播放到哪个时间了
- seekbar进度条
八:控制播放器前台和后台播放逻辑
播放器与Activity声明周期绑定
- 功能实现
当页面不可见时(app进入后台时),暂停播放;
当页面可见时,继续播放
页面销毁的时候释放资源:handler.removeCallbacks(runnable)
用于移除消息队列中指定的 Runnable
任务,这里的主要作用是停止进度更新的周期性循环。
@Overrideprotected void onDestroy() {super.onDestroy();player.release();//页面销毁的时候停止进度更新handler.removeCallbacks(runnable);}//当页面不可见时(app进入后台时),暂停播放@Overrideprotected void onStop() {super.onStop();player.setPlayWhenReady(false);}//当页面可见时,继续播放@Overrideprotected void onStart() {super.onStart();player.setPlayWhenReady(true);}
九:关联控制播放的 UI 组件
1:引入依赖
把ExoPlayer和三方库的一个可视化播放组件关联起来
implementation ("androidx.media3:media3-ui:1.0.0)
<androidx.media3.ui.PlayerViewandroid:id="@+id/player_view"android:layout_width="match_parent"android:layout_height="0dp"app:layout_constraintBottom_toBottomOf="parent"app:layout_constraintStart_toStartOf="parent"app:layout_constraintTop_toBottomOf="@+id/seek_bar"/>
效果如下
2:源码
本质上也是一个FrameLayout布局容器,它可以帮我们实现一些播放暂停的样式
3:使用方式
其实就是一个布局容器,把playerView和player做关联,player的状态有变化的时候,playerView也会随之改变
注:这里找控件的步骤省略了