在Android应用中使用Jepack Media3 ExoPlayer播放媒体文件

一、         实验名称

 在Android应用中使用Jepack Media3 ExoPlayer播放媒体文件。

二、         参考资料

Media3 ExoPlayer  |  Android media  |  Android Developers (google.cn)》、第九章课件。

三、         实验目的

练习在Android应用开发中使用Jepack Media3 ExoPlayer播放媒体文件。

四、         实验内容

本实验在第九章示例项目MediaApp的基础上完成。MediaApp只能播放单个的音频文件和视频文件,而且要播放的文件是在首页的播放按钮的事件处理中固定写好的,如下图所示:

image.png

接下来,我们通过以下步骤来完善MediaApp应用,使其能够播放音频文件列表和视频文件列表:

        在res目录下的raw目录中添加至少三个音频文件和三个视频文件(文件不要太大,否则提交 项目压缩包时文件会超大)。

        在项目包(com.example.mediaapp)下新建data包,在该包新建MediaDataSource.kt文件,在该文件中创建名为audioList和videoList的音频文件和视频文件的列表,列表项的格式如下所示:

"android.resource://context.packageName/{R.raw.music}"

        修改MainViewModel类,使其能够把MediaDataSource.kt文件中的音频列表和视频列表转换成界面状态数据。

        修改首页的播放音频按钮和播放视频按钮的事件处理程序,使其只有导航到媒体播放界面的功能。

        修改MediaPlayerScreen.kt文件中的相关函数,使其具有播放媒体文件列表的功能。

按照实验任务书的要求完成以下实验报告:

一、程序代码

 1. 修改HomeScreen.kt文件

package com.example.mediaapp.ui

import android.content.Context
import android.net.Uri
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import coil.compose.AsyncImage
import com.example.mediaapp.R
import com.example.mediaapp.ui.navigation.HomeDestination
import com.example.mediaapp.ui.utils.SHORT_MESSAGE
import com.example.mediaapp.ui.utils.makeStatusNotification

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(
   navigateToCamera: () -> Unit,
   navigateToMediaPlayer: () -> Unit,
   viewModel: MainViewModel,
   modifier: Modifier = Modifier
) {
   val context = LocalContext.current
   LaunchedEffect(Unit) {
       viewModel.loadMediaData(context)
   }

   val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
   Scaffold(
       topBar = {
           MediaAppTopAppBar(
               title = stringResource(HomeDestination.titleRes),
               canNavigateBack = false,
               scrollBehavior = scrollBehavior,
           )
       },
       modifier = modifier
   ) { innerPadding ->
       HomeBody(
           navigateToCamera = navigateToCamera,
           navigateToMediaPlayer = navigateToMediaPlayer,
           viewModel = viewModel,
           modifier = Modifier
               .padding(
                  innerPadding
               ).fillMaxSize()
               .verticalScroll(rememberScrollState())
       )
   }
}

@Composable
fun HomeBody(
   navigateToCamera: () -> Unit,
   navigateToMediaPlayer: () -> Unit,
   context: Context = LocalContext.current,
   viewModel: MainViewModel,
   modifier: Modifier = Modifier
) {
   val photoUri by viewModel.photoUri.collectAsStateWithLifecycle()
   Column(
       modifier = modifier
           .fillMaxSize(),
       verticalArrangement = Arrangement.Center,
       horizontalAlignment = Alignment.CenterHorizontally,
   ) {
       photoUri?.let {
           AsyncImage(
               model = it,
               contentDescription = "Selected image",
               modifier = Modifier
                   .fillMaxWidth()
                   .weight(1f),
           )
       }
       Button(
           onClick = { makeStatusNotification(SHORT_MESSAGE, context) },
       ) {
           Text(stringResource(id = R.string.send_notification))
       }
       Button(
           onClick = navigateToCamera,
       ) {
           Text(stringResource(id = R.string.call_camera))
       }
       ChooseImage(viewModel = viewModel)
       Button(
           //onClick = navigateToMediaPlayer,
           onClick = {
               viewModel.setPlayingAudioList(true)
               navigateToMediaPlayer()
           },
       ) {
           Text(stringResource(id = R.string.play_audio))
       }
       Button(
           //onClick = navigateToMediaPlayer,
           onClick = {
               viewModel.setPlayingAudioList(false)
               navigateToMediaPlayer()
           },
       ) {
           Text(stringResource(id = R.string.play_video))
       }
   }
}
/**
* 选择图片功能的界面组件。
*
* 该函数使用了Jetpack Compose进行界面构建,它与[MainViewModel]配合使用来处理选择图片的逻辑。
* 当用户点击按钮时,会调用系统相册等应用以选择图片,并将选中的图片URI更新到视图模型中。
*
* @param viewModel 用于处理界面逻辑和数据的[MainViewModel]实例。
*/
@Composable
fun ChooseImage(viewModel: MainViewModel) {
   val imagePicker =
       rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) { uri: Uri? ->
           uri?.let { viewModel.updatePhotoUri(it) }
       }
   Button(
       onClick = { imagePicker.launch("image/*") },
   ) {
       Text(stringResource(id = R.string.call_album))
   }
}

2. 修改MainViewModell类

package com.example.mediaapp.ui

import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import com.example.mediaapp.data.MediaDataSource
import android.net.Uri

class MainViewModel : ViewModel() {
   private val _photoUri = MutableStateFlow<Uri?>(null)
   val photoUri: StateFlow<Uri?> get() = _photoUri

   private val _mediaUri = MutableStateFlow<Uri?>(null)
   val mediaUri: StateFlow<Uri?> get() = _mediaUri

   private val _audioList = MutableStateFlow<List<String>>(emptyList())
   val audioList: StateFlow<List<String>> get() = _audioList

   private val _videoList = MutableStateFlow<List<String>>(emptyList())
   val videoList: StateFlow<List<String>> get() = _videoList

   private val _isPlayingAudioList = MutableStateFlow(false)
   val isPlayingAudioList: StateFlow<Boolean> get() = _isPlayingAudioList

   fun loadMediaData(context: Context) {
       viewModelScope.launch {
           _audioList.value = MediaDataSource.getaudioList(context)
           _videoList.value = MediaDataSource.getvideoList(context)
       }
   }

   fun updatePhotoUri(uri: Uri?) {
       viewModelScope.launch {
           _photoUri.value = uri
       }
   }

   fun updateMediaUri(uri: Uri?) {
       viewModelScope.launch {
           _mediaUri.value = uri
       }
   }

   fun setPlayingAudioList(isPlaying: Boolean) {
       viewModelScope.launch {
           _isPlayingAudioList.value = isPlaying
       }
   }
}

3. 在项目包(com.example.mediaapp)下新建data包,在该包新建MediaDataSource.kt文件

package com.example.mediaapp.data

import android.content.Context
import android.net.Uri
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import com.example.mediaapp.R
import com.example.mediaapp.ui.MainViewModel

object MediaDataSource {
   fun getaudioList(context: Context): List<String> {
       return listOf(
           "android.resource://context.packageName/{R.raw.music}",
           "android.resource://context.packageName/{R.raw.music1}",
           "android.resource://context.packageName/{R.raw.music2}",
           "android.resource://context.packageName/{R.raw.music3}",
       )
   }
   fun getvideoList(context: Context): List<String> {
       return listOf(
           "android.resource://context.packageName/{R.raw.video}",
           "android.resource://context.packageName/{R.raw.video1}",
           "android.resource://context.packageName/{R.raw.video2}",
           "android.resource://context.packageName/{R.raw.video3}"
           )
   }
}

4. 修改Media3Player.kt文件

package com.example.mediaapp.ui
import android.net.Uri
import android.view.View
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Scaffold
import androidx.compose.material3.TopAppBarDefaults

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.media3.common.MediaItem
import androidx.media3.common.Player
import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.ui.PlayerView
import com.example.mediaapp.ui.navigation.MediaPlayerDestination

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MediaPlayerScreen(
   navigateBack: () -> Unit,
   modifier: Modifier = Modifier,
   viewModel: MainViewModel
) {
   //val mediaUri by viewModel.mediaUri.collectAsStateWithLifecycle()
   val audioList by viewModel.audioList.collectAsStateWithLifecycle()
   val videoList by viewModel.videoList.collectAsStateWithLifecycle()
   val isPlayingAudioList by viewModel.isPlayingAudioList.collectAsStateWithLifecycle()
   val mediaList = if (isPlayingAudioList) audioList else videoList

   val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior()
   var mediaPlayer by rememberSaveable { mutableStateOf<ExoPlayer?>(null) }
   val context = LocalContext.current

   LaunchedEffect(mediaList) {
       mediaPlayer = ExoPlayer.Builder(context).build().apply {
           //val mediaItem = MediaItem.fromUri(mediaUri!!)
           val mediaItems = mediaList.map { MediaItem.fromUri(Uri.parse(it)) }
           setMediaItems(mediaItems)
           prepare()
       }
   }
   Scaffold(
       topBar = {
           MediaAppTopAppBar(
               title = stringResource(MediaPlayerDestination.titleRes),
               canNavigateBack = true,
               scrollBehavior = scrollBehavior,
               navigateUp = {
                   if (mediaPlayer != null) {
                       mediaPlayer?.release()
                       mediaPlayer = null
                   }
                   navigateBack()
               }
           )
       },
       modifier = modifier
   ) { innerPadding ->
       Column(
           modifier = modifier.padding(innerPadding),
           horizontalAlignment = Alignment.CenterHorizontally,
           verticalArrangement = Arrangement.Center
       ){
           if (mediaPlayer != null) {
               Media3Player(
                   player = mediaPlayer!!,
                   modifier = Modifier.fillMaxSize(),
                   customViewModifier = {}
               )
           }
       }
   }
}


/**
* 基于Jetpack Compose的媒体播放器组件。
*
* @param player 播放器实例,用于控制媒体播放。
* @param modifier 组件的修饰符,可用于设置布局属性,默认为Modifier。
* @param showController 是否显示控制器,默认为true。
* @param controllerVisibility 控制器的可见性,默认为View.VISIBLE。
* @param customViewModifier 一个函数,可用于自定义PlayerView的属性。
*/
@Composable
fun Media3Player(
   player: Player,
   modifier: Modifier = Modifier,
   showController: Boolean = true,
   controllerVisibility: Int = View.VISIBLE,
   customViewModifier: (PlayerView) -> Unit
) {
   val rememberedPlayer = remember { player }
   AndroidView(
       factory = { context ->
           PlayerView(context).apply {
               setPlayer(rememberedPlayer)
               useController = showController
               visibility = controllerVisibility
               customViewModifier(this)
           }
       },
       update = { view ->
           view.setPlayer(rememberedPlayer)
           view.useController = showController
           view.visibility = controllerVisibility
       },
       modifier = modifier
   )
}

5. 修改MediaApp.kt文件

package com.example.mediaapp.ui

import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.Text
import androidx.compose.material3.TopAppBarScrollBehavior
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.navigation.compose.rememberNavController
import com.example.mediaapp.ui.navigation.NavHost

@Composable
fun MediaApp() {
   val navController = rememberNavController()
   NavHost(navController = navController)
}

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MediaAppTopAppBar(
   title: String,
   canNavigateBack: Boolean,
   modifier: Modifier = Modifier,
   scrollBehavior: TopAppBarScrollBehavior? = null,
   navigateUp: () -> Unit = {}
) {
   CenterAlignedTopAppBar(
       title = { Text(title) },
       modifier = modifier,
       scrollBehavior = scrollBehavior,
       navigationIcon = {
           if (canNavigateBack) {
               IconButton(onClick = navigateUp) {
                   Icon(
                       imageVector = Icons.Filled.ArrowBack,
                       contentDescription = null
                   )
               }
           }
       }
   )
}

二、  实验结果(含程序运行截图)

1. 首页

新的图片

2. 调用相机

3. 播放音频

4. 播放视频

三、 出现问题及解决方法

 问题1:网络错误

错误分析:网络连接不稳定或速度慢;媒体文件的URL无效或不可访问;网络权限未正确配置。

解决方法:确保网络连接稳定且速度快。检查媒体文件的URL是否正确,并确保服务器可访问。

问题2:媒体文件错误

错误分析:视频或音频解码失败,提示媒体格式不支持或损坏。

解决方法:检查媒体文件是否兼容ExoPlayer支持的格式;尝试使用其他媒体文件进行测试,确保文件未损坏。

问题3:设备兼容性问题

错误分析:在某些设备上播放正常,在其他设备上播放失败。

解决方法:不同设备的硬件和软件配置差异导致兼容性问题。

四、  实验心得

    在res目录下的raw目录中添加至少三个音频文件和三个视频文件(文件不要太大,否则提交 项目压缩包时文件会超大);在项目包(com.example.mediaapp)下新建data包,在该包新建MediaDataSource.kt文件,在该文件中创建名为audioList和videoList的音频文件和视频文件的列表;修改MainViewModel类,使其能够把MediaDataSource.kt文件中的音频列表和视频列表转换成界面状态数据;修改首页的播放音频按钮和播放视频按钮的事件处理程序,使其只有导航到媒体播放界面的功能;修改MediaPlayerScreen.kt文件中的相关函数,使其具有播放媒体文件列表的功能。

    ExoPlayer功能强大与灵活性。ExoPlayer提供了丰富的功能和极高的灵活性,能够满足各种复杂的媒体播放需求。无论是本地文件播放、网络流媒体播放,还是多种媒体格式的支持,ExoPlayer都能轻松应对。同时,它还提供了丰富的API和扩展库,使得开发者能够根据自己的需求进行定制和优化。

    ExoPlayer优秀的性能表现:无论是启动速度、加载速度还是播放流畅度,都达到了很高的水平。即使在处理大文件或高码率的媒体文件时,也能保持稳定的播放效果,避免了卡顿和延迟等问题。

    良好的兼容性:ExoPlayer在兼容性方面也表现得相当出色。它支持多种Android设备和系统版本,能够在不同的硬件和软件环境下稳定运行。这使得开发者无需担心兼容性问题,可以更加专注于应用的功能和用户体验。

  • 22
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值