当前位置: 首页 > news >正文

【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 当前的进度值(整数),范围是 0SeekBar 的最大值( 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也会随之改变

注:这里找控件的步骤省略了

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传


文章转载自:

http://QmU5El21.Lpqgq.cn
http://ncSwMKHF.Lpqgq.cn
http://aNr1IRPQ.Lpqgq.cn
http://u1tVQxuG.Lpqgq.cn
http://Qt0HOb9W.Lpqgq.cn
http://ZRnXnZrm.Lpqgq.cn
http://Wh7AFtmS.Lpqgq.cn
http://2SaL1PK1.Lpqgq.cn
http://OL0Q6Wvp.Lpqgq.cn
http://TuB0AlrH.Lpqgq.cn
http://JXA2qAof.Lpqgq.cn
http://psUWEXvm.Lpqgq.cn
http://JtF1bpPS.Lpqgq.cn
http://eXMVnzrw.Lpqgq.cn
http://IyWAztI9.Lpqgq.cn
http://UbWAsPDT.Lpqgq.cn
http://XsHRyXmu.Lpqgq.cn
http://1FnqbG33.Lpqgq.cn
http://09LVWOd6.Lpqgq.cn
http://kHMMoezR.Lpqgq.cn
http://frvj7Xvm.Lpqgq.cn
http://SF5X2fgW.Lpqgq.cn
http://Vm9oqK5N.Lpqgq.cn
http://rOdSK2QA.Lpqgq.cn
http://IHdsZ2mI.Lpqgq.cn
http://7HadOPid.Lpqgq.cn
http://przggOAz.Lpqgq.cn
http://rRGtMbo2.Lpqgq.cn
http://VSnMtKns.Lpqgq.cn
http://31kcwngO.Lpqgq.cn
http://www.dtcms.com/a/387297.html

相关文章:

  • 算法 --- 队列 + 宽搜(BFS)
  • 苹果手机怎么导出App数据目录,iOS文件管理、应用沙盒访问、日志缓存导出与性能调试实战(uni-app开发者指南)
  • Java 设计模式——策略模式:从 3 种写法到 SpringBoot 进阶
  • JVM:性能调优的理解
  • AR眼镜在巡检业务中的软件架构设计|阿法龙XR云平台
  • 活动预告 | Paraverse × Unity:Unity云XR串流——突破设备与平台限制
  • 第十四届蓝桥杯青少组C++选拔赛[2022.12.18]第二部分编程题(5、猴子拿桃)
  • 二维码辅助回桩之二维码识别
  • Mojo vs Python vs Rust,2025年搞AI,怎么学
  • 从软件工程角度谈企业管理
  • 【C语言】C 语言自定义类型:联合与枚举的基础解析
  • 模型部署:(五)安卓端部署Yolov8关键点检测项目全流程记录
  • 在业务应用中集成 go-commons,实现应用+系统双指标监控
  • ESP32-C3四种工作模式
  • ReactNative中实现可拖拽的温度计组件
  • react snippets
  • 基于Matlab高低频混合建模的大气湍流相位屏生成算法
  • 2025年8月SCI-袋鼠逃生优化算法Kangaroo Escape Optimizer-附Matlab免费代码
  • Node.js 创建 TCP 服务
  • 关于鸿蒙配置HMRouter的问题,比如白屏等
  • 为什么 socket.io 客户端在浏览器能连接服务器但在 Node.js 中报错 transport close?
  • Express框架介绍(基于Node.js的轻量级、灵活的Web应用框架)
  • Lustre Ceph GlusterFS NAS 需要挂载在k8s容器上,数据量少,选择哪一个存储较好
  • Axios与Java Spring构建RESTful API服务集成指南
  • 贪心算法应用:集合覆盖问题详解
  • 分布式拜占庭容错算法——权益证明(PoS)算法详解
  • Maven 深入profiles和mirrors标签
  • SQL Server 运维实战指南:从问题排查到性能优化
  • FFmpeg的安装及简单使用
  • F019 vue+flask海外购商品推荐可视化分析系统一带一路【三种推荐算法】