当前位置: 首页 > 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也会随之改变

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

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

引入

1:需求

利用Jetpack封装好的PlayView做一款视频播放器,进一步使用自己设置的空间关联播放器

2:依赖

    implementation("androidx.media3:media3-exoplayer:1.0.0")implementation ("androidx.media3:media3-ui:1.0.0")

二:前端UI界面

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

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout 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=".VideoActivity"><androidx.media3.ui.PlayerViewandroid:id="@+id/player_view"android:layout_width="match_parent"android:layout_height="0dp"android:layout_weight="1"app:use_controller="false"android:background="@color/black" /><RelativeLayoutandroid:layout_width="match_parent"android:layout_height="wrap_content"><SeekBarandroid:id="@+id/seek_bar"android:layout_width="match_parent"android:layout_height="wrap_content" /><ImageViewandroid:id="@+id/iv_control"android:layout_width="44dp"android:layout_height="44dp"android:layout_below="@+id/seek_bar"android:layout_centerHorizontal="true"android:src="@mipmap/icon_bofang" /></RelativeLayout></LinearLayout>

1:use_controller

设置为false,不使用也不显示播放器内提供视频播放按钮组件(比如播放、暂停按钮,进度条等),下图是对比

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

三:代码


public class VideoActivity extends AppCompatActivity {private static final String TAG = "VideoActivity";private PlayerView playerView;private SeekBar seekBar;private ExoPlayer player;private ImageView imageContrl;@Overrideprotected void onCreate(Bundle savedInstanceState) {super.onCreate(savedInstanceState);setContentView(R.layout.activity_video);imageContrl = findViewById(R.id.iv_control);playerView = findViewById(R.id.player_view);seekBar = findViewById(R.id.seek_bar);initPlayer();//添加资源文件到player中ArrayList<MediaItem> mediaItems = new ArrayList<>();MediaItem mediaItem1 = MediaItem.fromUri(Uri.parse("http://titok.fzqq.fun//uploads/20241010/b054fb0478e8c3d95a518cf5e1b67163.mp4"));MediaItem mediaItem2 = MediaItem.fromUri(Uri.parse("http://titok.fzqq.fun//uploads/20241010/5043fbca9fabb94fba2668c4a827d3a5.mp4"));MediaItem mediaItem3 = MediaItem.fromUri(Uri.parse("http://titok.fzqq.fun//uploads/20241010/02577b1fabe3cbbdd75c3f6a02434efd.mp4"));mediaItems.add(mediaItem1);mediaItems.add(mediaItem2);mediaItems.add(mediaItem3);player.setMediaItems(mediaItems);player.prepare();imageContrl.setOnClickListener(new View.OnClickListener() {@Overridepublic void onClick(View v) {if(player.isPlaying()){player.pause();imageContrl.setImageResource(R.mipmap.icon_zanting);}else {player.play();imageContrl.setImageResource(R.mipmap.icon_bofang);}}});}private void initPlayer(){player = new ExoPlayer.Builder(this).build();player.addListener(new Player.Listener() {@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:player.play();long duration = player.getDuration();Log.i(TAG, "onPlaybackStateChanged: 播放准备就绪,当前资源总时长为" +duration);break;case Player.STATE_ENDED:Log.i(TAG, "onPlaybackStateChanged: 播放器播放完毕");break;}}@Overridepublic void onPlayerError(PlaybackException error) {Log.i(TAG, "onPlayerError: error = " + error);if(error.errorCode == PlaybackException.ERROR_CODE_TIMEOUT){Toast.makeText(VideoActivity.this,"加载超时",Toast.LENGTH_SHORT).show();}}@Overridepublic void onIsPlayingChanged(boolean isPlaying) {Log.i(TAG, "onIsPlayingChanged: isPlaying = " + isPlaying);}});playerView.setPlayer(player);}@Overrideprotected void onStart() {super.onStart();player.setPlayWhenReady(true);}@Overrideprotected void onStop() {super.onStop();player.setPlayWhenReady(false);}@Overrideprotected void onDestroy() {super.onDestroy();player.release();}
}

1:初始化

跟之前学习实现音频播放是一种套路,使用构建者设计模式,添加监听器,重写三个方法,算固定写法了,整体的执行流程如下

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

注意一点的是,当视频资源加载完成并且初始化也完成后,视频会自动开始播放

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

2:视频源

playview播放的是视频还是音频并没有太大的区别,主要是来源问题

这里我们去解析网络资源,并添加到ArrayList中,设置给player

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

3:自创组件关联视频播放器

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

4:问题

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

猜测用Medium Phone API 36.0模拟机并不能进行网络访问,感觉是虚拟网络设置相关的问题;之前也遇到过这种情况,使用Pixel 8 Pro就可以正常进行

http://www.dtcms.com/a/389195.html

相关文章:

  • Android 开发 集成 uni 小程序,并实现相互通信
  • 【office】怎么设置第一章二级标题为1.1 1.2 1.3然后第二章为2.1 2.2 2.3这样子
  • JVM的垃圾回收机制(一次完整的GC流程)
  • 拥抱新一代 Web 3D 引擎,Three.js 项目快速升级 Galacean 指南
  • Linux 内核裁剪与功能扩展实验报告
  • Qt QVCandlestickModelMapper详解
  • LeetCode:20.旋转图像
  • 网络协议深度解析:从OSI七层模型到现代互联网通信的技术实战
  • 慈明学校以孝治家阳光家庭教育中心 学以致用践行以孝治家幸福万家
  • 开心实习之 深度学习之多层感知机
  • 前端构造数据格式及表格添加行
  • 深度学习-神经网络(上篇)
  • 【脑电分析系列】第18篇:传统机器学习在EEG中的应用 — SVM、LDA、随机森林等分类器
  • 理解长短期记忆神经网络(LSTM)
  • Kurt-Blender零基础教程:第2章:建模篇——第1节:点线面的选择与控制与十大建模操作
  • 鸿蒙5.0应用开发——V2装饰器@Monitor的使用
  • 八、Java-XML
  • 计算机在医疗领域应用的独特技术问题分析
  • HTB Intentions writeup(SQL二次注入也是注入)
  • 第一章 预训练:让模型“博闻强识”
  • 【数组】求两个匀速运动质点的相交或最小距离
  • 新手向:Python爬虫原理详解,从零开始的网络数据采集指南
  • OKZOO进军HealthFi:承接AIoT,引领Health-to-Earn
  • Halcon 相机标定
  • 腾讯混元发布集成翻译模型Hunyuan-MT-Chimera-7B,已开放体验
  • mybatis-plus扩展
  • 从x.ai到VSCode:一个AI编程助手的意外之旅
  • SQLite vs MySQL:核心SQL语法差异全面解析
  • 【每日算法】两数相加 LeetCode
  • ActiveMQ底层原理与性能优化