安卓多媒体(音频录播、传统摄制、增强摄制)

news2024/11/25 22:26:01

本章介绍App开发常用的一些多媒体处理技术,主要包括:如何录制和播放音频,如何使用传统相机拍照和录像,如何截取视频画面,如何使用增强相机拍照和录像。

音频录播

本节介绍Android对音频的录播操作,内容包括如何使用系统录音机录制音频、如何利用MediaPlayer播放音频、如何使用MediaRecorder录制音频。

使用系统录音机录制音频

手机自带的系统相机,也有自带的系统录音机,录音机对应的意图动作为MediaStore.Audio.Media.RECORD_SOUND_ACTION,只要在调用startActivityForResult之前指定该动作,就会自动跳转到系统的录音机界面。下面便是前往系统录音机的跳转代码例子:

// 下面打开系统自带的录音机
Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
startActivityForResult(intent, RECORDER_CODE); // 跳到录音机页面

注意上面的RECORDER_CODE是自定义的一个常量值,表示录音来源,目的是在onActivityResult方法中区分唯一的请求码。接着重写活动页面的onActivityResult方法,添加以下的回调代码获取录制好的音频:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
    super.onActivityResult(requestCode, resultCode, intent);
    if (resultCode==RESULT_OK && requestCode==RECORDER_CODE){
        mAudioUri = intent.getData(); // 获得录制好的音频uri
        String filePath = String.format("%s/%s.mp3",
                getExternalFilesDir(Environment.DIRECTORY_MUSIC), "audio_"+ DateUtil.getNowDateTime());
        FileUtil.saveFileFromUri(this, mAudioUri, filePath); // 保存为临时文件
        tv_audio.setText("录制完成的音频地址为:"+mAudioUri.toString());
        iv_audio.setVisibility(View.VISIBLE);
    }
}

从以上代码可知,录制完的音频路径就在返回意图的getData当中,那么怎样验证这个路劲保存的是音频呢?当然是听听该音频能否正常播放就对了。所谓好事成双,既有录音机,又有收音机,音频自然由系统自带的收音机播放了。若想自动跳转到收音机界面,关键是把数据类型设置为音频,系统才知晓原来是要打开音频,这活还是交给收音机吧。打开系统收音机的跳转代码如下:

// 下面打开系统自带的收音机
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(mAudioUri, "audio/*"); // 类型为音频
startActivity(intent); // 跳到收音机页面

接下来通过实验来看录音与播音的完整过程,点击“打开录音机”按钮之后,跳转到如下图所示的录音机界面。
在这里插入图片描述
点击底部的圆形按钮开始录音,稍等几秒再次点击该按钮结束录音,此时屏幕底部弹出如下图所示的选择对话框。
在这里插入图片描述
点击选择对话框中的“使用此录音”选线,回到测试App界面,如下图所示,可见回调代码成功获得刚录制得音频路径。
点击页面上的三角播放按钮,跳转到如下图的收音机界面,同时收音机开始播放音频。
在这里插入图片描述

利用MediaPlayer播放音频

尽管让App跳转到收音机界面就能播放音频,但是通常App都不希望用户离开自身页面,何况播音本来仅是一个小功能,完全可以一边播放音频一边操作界面。若要在App内部自己播音,便用到了媒体播放器MediaPlayer,不过在播放音频之前,得先想办法找到音频文件才行。通过内容解析器能够从媒体库查找图片文件,同样也能从媒体库查找音频文件,只要把相关条件换成音频种类就成,例如把媒体库得Uri路径从相册换成音频库,把媒体库的查找结果从相册字段换作音频字段等。为此另外定义并声明音频类型的实体对象,声明代码如下:

private List<AudioInfo> mAudioList = new ArrayList<AudioInfo>(); // 音频列表
private Uri mAudioUri = MediaStore.Audio.Media.EXTERNAL_CONTENT_URI; // 音频库的Uri
private String[] mAudioColumn = new String[]{ // 媒体库的字段名称数组
        MediaStore.Audio.Media._ID, // 编号
        MediaStore.Audio.Media.TITLE, // 标题
        MediaStore.Audio.Media.DURATION, // 播放时长
        MediaStore.Audio.Media.SIZE, // 文件大小
        MediaStore.Audio.Media.DATA}; // 文件路径
private MediaPlayer mMediaPlayer = new MediaPlayer(); // 媒体播放器

接着通过内容解析器系统的音频库,把符合条件的音频记录依次添加到音频列表,下面便是从媒体库加载音频文件列表的代码例子:

// 加载音频列表
private void loadAudioList() {
    mAudioList.clear(); // 清空音频列表
    // 通过内容解析器查询音频库,并返回结果集的游标。记录结果按照修改时间降序返回
    Cursor cursor = getContentResolver().query(mAudioUri, mAudioColumn,
            null, null, "date_modified desc");
    if (cursor != null) {
        // 下面遍历结果集,并逐个添加到音频列表。简单起见只挑选前十个音频
        for (int i=0; i<10 && cursor.moveToNext(); i++) {
            AudioInfo audio = new AudioInfo(); // 创建一个音频信息对象
            audio.setId(cursor.getLong(0)); // 设置音频编号
            audio.setTitle(cursor.getString(1)); // 设置音频标题
            audio.setDuration(cursor.getInt(2)); // 设置音频时长
            audio.setSize(cursor.getLong(3)); // 设置音频大小
            audio.setPath(cursor.getString(4)); // 设置音频路径
            mAudioList.add(audio); // 添加至音频列表
        }
        cursor.close(); // 关闭数据库游标
    }
}

找到若干音频文件之后,还要设法利用MediaPlayer来播音。MediaPlayer顾名思义叫作媒体播放器,它既能播放音频也能播放视频,其常用方法说明如下:

  • reset:重置播放器。
  • prepare:准备播放。
  • start:开始播放。
  • pause:暂停播放。
  • stop:停止播放。
  • create:创建指定Uri的播放器。
  • setDataSource:设置播放器数据来源的文件路径。create与setDataSource两个方法只需调用一个。
  • setVolume:设置音量。两个参数分别是左声道和右声道的音量,取值0~1。
  • setAudioStreamType:设置音频流的类型。音频流类型的取值说明见下表。
AudioManager类的铃音类型铃声名称说明
STREAM_VOICE_CALL通话音
STREAM_SYSTEM系统音
STREAM_RING铃声来电与收到短信的铃声
STREAM_MUSIC媒体音音乐、视频、游戏等的声音
STREAM_ALARM闹钟音
STREAM_NOTIFICATION通知音
  • setLooping:设置是否循环播放。true表示循环播放,false表示只播放一次。
  • isPlaying:判断是否正在播放。
  • getCurrentPosition:获取当前播放进度所在的位置。
  • getDuration:获取播放时长,单位为毫秒。

MediaPlayer提供的方法虽多,基本的应用场景只有两个:一个是播放指定音频文件,另一个是在退出页面时释放媒体资源。其中播放音频的场景需要经历下列步骤:重置播放器->设置媒体文件路径->准备播放->开始播放。对应的播放代码示例如下:

mMediaPlayer.reset(); // 重置媒体播放器
// mMediaPlayer.setVolume(0.5f, 0.5f); // 设置音量,可选
mMediaPlayer.setAudioStreamType(AudioManager.STREAM_MUSIC); // 设置音频流的类型为音乐
try {
    mMediaPlayer.setDataSource(audio.getPath()); // 设置媒体数据的文件路径
    mMediaPlayer.prepare(); // 媒体播放器准备就绪
    mMediaPlayer.start(); // 媒体播放器开始播放
} catch (Exception e) {
    e.printStackTrace();
}

如果没把音频放入后台服务中播放,那么在退出活动页面之时应当主动释放媒体资源,以便提高系统运行效率。此时可以重写活动的onDestroy方法,在该方法内部补充下面的操作代码:

if (mMediaPlayer.isPlaying()) { // 是否正在播放
    mMediaPlayer.stop(); // 结束播放
}
mMediaPlayer.release(); // 释放媒体播放器

当然,上述的两个场景之时两种最基础的运用,除此之外,还存在其他业务场合,包括但不限于:实时刷新当前的播放进度、将音频拖动到指定位置再播放、播放完毕之时提醒用户等,详细的演示代码参见AudioPlayActivity.java。下面是使用MediaPlayer播放音频的界面效果。其中左侧展示了刚打开的初始界面,此时App自动查找并罗列最新的音频文件;点击其中一项音频,App便开始播放该音频,同时下方实时显示播放进度如右侧图片所示。
在这里插入图片描述

利用MediaRecorder录制音频

与媒体播放器相对应,Android提供了媒体录制器MediaRecorder,它既能录制音频也能录制视频。使用MediaRecorder可以在当前页面直接录音,而不必跳转到系统自带的录音机界面。MediaRecorder的常用方法说明如下:

  • reset:重置录制器。
  • prepare:准备录制。
  • start:开始录制。
  • stop:结束录制。
  • release:释放录制器。
  • setMaxDuration:设置可录制的最大时长,单位为毫秒(ms)。
  • setMaxFileSize:设置可录制的最大文件大小,单位为字节(B)。setMaxDuration与setMaxFileSize设置其一即可。
  • setOutputFile:设置输出文件的保存路径。
  • setAudioSource:设置音频来源。一般使用麦克风AudioSource.MIC。
  • setOutputFormat:设置媒体输出格式。媒体输出格式的取值说明见下表。
OutputFormat类的输出格式格式分类扩展名格式说明
AMR_NB音频.arm窄带格式
AMR_WB音频.arm宽带格式
AAC_ADTS音频.aac高级的音频传输流格式
MPEG_4视频.mp4MPEG4格式
THREE_GPP视频.3gp3GP格式
  • setAudioEncoder:设置音频编码器。音频编码器的取值说明见下表。注意,该方法应在setOutputFormat方法之后执行,否则会抛出异常。
AudioEncoder类的音频编码器说明
AMR_NB窄带编码
AMR_WB宽带编码
AAC低复杂度的高级编码
HE_AAC高效率的高级编码
AAC_ELD高效率的高级编码
  • setAudioSamplingRate:设置音频的采样率,单位为千赫兹(kHz)。AMR_NB格式默认为8kHz,AMR_WB格式默认为16kHz。
  • setAudioChannels:设置音频每秒录制的字节数。数值越大音频越清晰。

MediaRecorder提供的方法虽多,基本的应用场景只有两个:一个是开始录制媒体文件,另一个是停止录制媒体文件。其中录制音频的场景需要经历下列步骤:重置录制器->设置媒体文件的路径->准备录制->开始录制,对应的录制代码示例如下:

// 获取本次录制的媒体文件路径
mRecordFilePath = MediaUtil.getRecordFilePath(this, "RecordAudio", ".amr");
// 下面是媒体录制器的处理代码
mMediaRecorder.reset(); // 重置媒体录制器
mMediaRecorder.setOnInfoListener(this); // 设置媒体录制器的信息监听器
mMediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // 设置音频源为麦克风
mMediaRecorder.setOutputFormat(mOutputFormat); // 设置媒体的输出格式。该方法要先于setAudioEncoder调用
mMediaRecorder.setAudioEncoder(mAudioEncoder); // 设置媒体的音频编码器
mMediaRecorder.setMaxDuration(mDuration * 1000); // 设置媒体的最大录制时长
mMediaRecorder.setOutputFile(mRecordFilePath); // 设置媒体文件的保存路径
try {
    mMediaRecorder.prepare(); // 媒体录制器准备就绪
    mMediaRecorder.start(); // 媒体录制器开始录制
} catch (Exception e) {
    e.printStackTrace();
}

至于停止录制操作,直接调用stop方法即可。当然,在退出活动页面之时,还需调用release方法释放录制资源。注意到上述的录制代码引用了若干变量,包括输出格式mOutputFormat、音频编码器mAudioEncoder、最大录制时长mDuration等,这些参数决定了音频文件的音效质量和文件大小,详细的演示例子参见代码MediaRecorderActivity.java。
运行测试App,保持默认的录制参数,点击“开始录制”按钮,正在录音的界面如下图左侧所示;稍等片刻录音完成的界面如下图右侧所示,此时成功保存录制好的音频文件,点击下方的三角播放按钮,就能通过MediaPlayer播音了。
在这里插入图片描述

传统摄制

本节介绍Android对照片和视频的传统摄制操作,内容包括如何使用系统相机拍摄照片(含缩略图和原始图两种方式)、如何使用系统摄像机录制视频、如何利用视频视图与媒体控制条播放视频、如何通过媒体检索工具截取视频画面。

使用系统相机拍摄照片

俗话说“眼睛是心灵的窗户”,那么摄像头便是手机的窗户了,一部手机美不美,很大程度上要看它的摄像头,因为好的摄像头才能拍摄出美丽的照片。对于手机拍照的App开发而言,则有两种实现方式:一种通过Camera工具联合表面视图SurfaceView自行规划编码细节;另一种是借助系统相机自动拍照。考虑到多数场景对图片并无特殊要求,因而使用系统相机更加方便快捷。
调用系统相机的方式也有初级与高级之分,倘若仅仅想看个大概,那么一张缩略图便已足够。下面便是打开相机的代码例子:

// 下面通过系统相机拍照只能获得缩略图
Intent photoIntent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE); 
startActivityForResult(photoIntent, THUMBNAIL_CODE); // 打开系统相机

注意上面的THUMBNAIL_CODE是自定义的一个常量值,表示缩略图来源,目的是在onActivityResult方法中区分唯一的请求代码。接着重写胡活动页面的onActivityResult方法,添加以下的回调代码获取缩略图对象:

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent intent) {
	super.onActivityResult(requestCode, resultCode, intent);
	if (RESULT_OK == resultCode && THUMBNAIL_CODE == requestCode) {
		// 缩略图放在返回意图中的data字段,将其取出转成位图对象即可
		Bundle extras = intent.getExtras();
		Bitmap bitmap = (Bitmap)extras.get("data");
		iv_photo.setImageBitmap(bitmap); // 设置图像视图的位图对象
	}
}

运行App,打开系统相册,此时定格的画面如下左图所示。点击屏幕右上角的打勾图标,返回App界面如下图右侧所示,果然显示刚才拍照的缩略图。
在这里插入图片描述
通过系统相机拍照获得缩略图就是这么简单,只是缩略图不够清晰,马马虎虎浏览一下尚可,要看得细致入微确实不能够了。若想得到高清大图,势必采取系统相机得高级用法,为此事先声明一个图片得Uri对象,声明代码如下:

private Uri mImageUri; // 图片的路径对象

接着在打开系统相机之前,传入图片得路径对象,表示拍好得图片保存在这个路径,具体得操作代码如下(注意安卓10得适配处理代码):

// Android10开始必须由系统自动分配路径,同时该方式也能自动刷新相册
ContentValues values = new ContentValues();
// 指定图片文件的名称
values.put(MediaStore.Images.Media.DISPLAY_NAME, "photo_"+DateUtil.getNowDateTime());
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg"); // 类型为图像
// 通过内容解析器插入一条外部内容的路径信息
mImageUri = getContentResolver().insert(
        MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
// 下面通过系统相机拍照可以获得原始图
photoIntent.putExtra(MediaStore.EXTRA_OUTPUT, mImageUri);
startActivityForResult(photoIntent, ORIGINAL_CODE); // 打开系统相机

以上的ORIGINAL_CODE依然是自定义得请求代码,表示原始图来源,然后重写活动页面的onActivityResult方法,补充下述的分支处理代码:

if (RESULT_OK == resultCode && ORIGINAL_CODE == requestCode) {
	// 根据指定图片的Uri,获得自动缩小后的位图对象
	Bitmap bitmap = BitmapUtil.getAutoZoomImage(this, mImageUri);
	iv_photo.setImageBitmap(bitmap); // 设置图像视图的位图对象
}

因为之前已经把图片的路径对象传给系统相机了,所以这里可以直接设置图像视图的路径对象,无须再去解析什么包裹信息。
重新运行测试App,打开系统相机后拍照,此时定额的画面如下左图。仍旧点击屏幕右上角的打勾图标,返回App界面如下右图所示,果然成功展示了拍摄的高清大图。
在这里插入图片描述

使用系统摄像机录制视频

与音频类似,通过系统摄像机可以很方便地录制视频,只要指定摄像动作为MediaStore.ACTION_VIDEO_CAPTURE即可。当然,也能事先设定下列的摄像参数:

  • MediaStore.EXTRA_VIDEO_QUALITY:用于设定视频质量。
  • MediaStore.EXTRA_SIZE_LIMIT:用于设定文件大小的上限。
  • MediaStore.EXTRA_DURATION_LIMIT:用于设定视频时长的上限。

下面是跳转到系统摄像机的代码例子:

// 声明一个活动结果启动器对象
private ActivityResultLauncher launcher = registerForActivityResult (
	new ActivityResultContracts.TakeVideo(), bitmap -> {
	    tv_video.setText("录制完成的视频地址为:"+mVideoUri.toString());
	    rl_video.setVisibility(View.VISIBLE);
	    if (bitmap == null) {
	        // 获取视频文件的某帧图片
	        bitmap = MediaUtil.getOneFrame(this, mVideoUri, 1000);
	    }
	    iv_video.setImageBitmap(bitmap);
	});

// 开始录制视频
private void takeVideo() {
    // Android10开始必须由系统自动分配路径,同时该方式也能自动刷新相册
    ContentValues values = new ContentValues();
    // 指定图片文件的名称
    values.put(MediaStore.Video.Media.DISPLAY_NAME, "video_"+DateUtil.getNowDateTime());
    values.put(MediaStore.Video.Media.MIME_TYPE, "video/mp4"); // 类型为视频
    // 通过内容解析器插入一条外部内容的路径信息
    mVideoUri = getContentResolver().insert(
            MediaStore.Video.Media.EXTERNAL_CONTENT_URI, values);
    launcher.launch(mVideoUri);
}

视频录制完成,最好能够预览视频的摄制画面,所以上面代码调用了getOneFrame方法获取视频文件的某帧图片,查看该帧图像即可大致了解视频内容。抽取视频帧图的getOneFrame方法代码如下:

    // 获取视频文件中的某帧图片。pos为毫秒时间
    public static Bitmap getOneFrame(Context ctx, Uri uri, int pos) {
        MediaMetadataRetriever retriever = new MediaMetadataRetriever();
        retriever.setDataSource(ctx, uri); // 将指定Uri设置为媒体数据源
        // 获取指定时间的帧图,注意getFrameAtTime方法的时间单位是微秒
        Bitmap bitmap = retriever.getFrameAtTime(pos * 1000);
        return bitmap;
    }

有了视频文件的Uri之后,就能利用系统自带的播放器观看视频了。同样设置意图动作Intent.ACTION_VIEW,并指定数据类型为视频,以下几行代码即可打开视频播放器:

// 创建一个内容获取动作的意图(准备跳到系统播放器)
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(mVideoUri, "video/*"); // 类型为视频
startActivity(intent); // 打开系统的视频播放器

运行App,点击“打开摄像机”按钮之后,跳转到如下图左侧所示的系统摄像界面,点击界面下方中央的圆形按钮开始录像,稍等几秒再次按下该按钮,或者等待EXTRA_DURATION_LIMIT设定的时长到达,此时摄像结束的界面如下图右侧所示。
在这里插入图片描述
点击录像界面右上角的打勾图标,回到App的演示界面,发现原页面展示了已枯枝视频的快照图像。单击该快照图片表示期望播放视频,即可播放录制的视频。

利用视频视图与媒体控制条播放视频

通过专门的播放器固然能够播放视频,但要离开当前App跳转到播放器界面才行,因为视频播放不算很复杂的功能,人们更希望内嵌在当前App界面,所以Android提供了名为视频视图(VideoView)的播放控件,该控件允许图像视图那样划出一块界面展示视频,同时还支持对视频进行播放控制,为开发者定制视频操作提供了便利。
下面是VideoView的常用方法:

  • setVideoURI:设置视频文件的URI路径。
  • setVideoPath:设置视频文件的字符串路径。
  • setMediaController:设置媒体控制条的对象。
  • start:开始播放视频。
  • pause:暂停播放视频。
  • resume:恢复播放视频。
  • suspend:结束播放并释放资源。
  • getDuration:获得视频的总时长,单位为毫秒。
  • getCurrentPosition:获得当前的播放位置。返回值若等于总时长,表示播放到了末尾。
  • isPlaying:判断视频是否正在播放。

由于VideoView只显示播放界面,没显示控制按钮和进度条,因此在实际开发中需要给她配备媒体控制条MediaController。该控制条支持基本的播放控制操作,包括:显示当前的播放进度、拖动到指定位置播放、暂停播放与恢复播放、查看视频的总时长和已播放时长、对视频做快进或快退操作等。
下面是MediaController的常用方法说明:

  • setMediaPlayer:设置媒体播放器的对象,也就是指定某个VideoView。
  • show:显示媒体控制条。
  • hide:隐藏媒体控制条。
  • isShowing:判断媒体控制条是否正在显示。

将媒体控制条与视频图集成起来的话,一般让媒体控制条固定放在视频视图的底部。此时无须在XML文件中添加MediaController节点,只需要添加VideoView节点,然后在Java代码中将媒体控制条附着于视频视图即可。具体的集成步骤分为下列4步:

  1. 由视频对象调用setVideoURI方法指定视频文件。
  2. 创建一个媒体控制条,并由视频视图对象调用setMediaController方法关联该控制条。
  3. 由控制条对象调用setMediaPlayer方法,将媒体播放器设置为该视频视图。
  4. 调用视频视图对象的start方法,开始播放视频。

接下来实验看看如何通过视频视图播放视频。首先创建测试活动页面,在该页面的XML文件中添加VideoView节点,完整的XML内容如下:

<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:orientation="vertical">
    <Button
        android:id="@+id/btn_choose"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="打开相册播放视频"
        android:textColor="@color/black"
        android:textSize="17sp" />
    <VideoView
        android:id="@+id/vv_content"
        android:layout_width="match_parent"
        android:layout_height="wrap_content" />
</LinearLayout>

然后往该页面的活动代码补充选视频库之后的回调逻辑,也就是重写registerForActivityResult回调方法,在该方法内部设置视频图的视频路径,关联媒体控制条,再调用时评视图的start方法播放视频。详细的活动页面代码示例如下:

public class VideoPlayActivity extends AppCompatActivity {
    private final static String TAG = "VideoPlayActivity";
    private VideoView vv_content; // 声明一个视频视图对象

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_play);
        // 从布局文件中获取名叫vv_content的视频视图
        vv_content = findViewById(R.id.vv_content);
        // 注册一个善后工作的活动结果启动器,获取指定类型的内容
        ActivityResultLauncher launcher = registerForActivityResult(
                new ActivityResultContracts.GetContent(), uri -> {
                    if (uri != null) {
                        playVideo(uri); // 播放视频
                    }
                });
        findViewById(R.id.btn_choose).setOnClickListener(v -> launcher.launch("video/*"));
    }

    private void playVideo(Uri uri) {
        vv_content.setVideoURI(uri); // 设置视频视图的视频路径
        MediaController mc = new MediaController(this); // 创建一个媒体控制条
        vv_content.setMediaController(mc); // 给视频视图设置相关联的媒体控制条
        mc.setMediaPlayer(vv_content); // 给媒体控制条设置相关联的视频视图
        vv_content.start(); // 视频视图开始播放
    }
}

运行测试App,打开初始的视频界面如下图最左侧所示,此时按钮下方没有黑漆漆的一片都是视频视图区域;点击“打开相册播放视频”按钮从视频库选择视频回来,该界面立即开始播放选中的视频,如下图中间图片;在视频区域轻轻点击,此时视频下方弹出一排媒体控制条,如下图最右侧所示,可见媒体控制条上半部分有快进、暂停、快退 3个按钮,下半部分展示了当前播放时长、播放进度条、视频总时长。
在这里插入图片描述

截取视频的某帧画面

不管是系统相册还是视频网站,在某个视频尚未播放的时候都会显示一张预览图片,该图片通常是视频中的某个画面。Android从视频中截取某帧画面,用到了媒体检索工具MediaMetadataRetriever,它的常见方法分别说明如下:

  • setDataSource:将指定URI设置为媒体数据源。
  • extractMetadata:获得视频的播放时长。
  • getFrameAtIndex:获取指定索引的帧图。
  • getFrameAtTime:获取指定时间的帧图,时间单位为微秒。
  • release:释放媒体资源。

下面是利用MediaMetadataRetriever从视频截取某帧位图的示例代码:

// 获取视频文件中的某帧图片。pos为毫秒时间
public static Bitmap getOneFrame(Context ctx, Uri uri, int pos) {
    MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    retriever.setDataSource(ctx, uri); // 将指定Uri设置为媒体数据源
    // 获取指定时间的帧图,注意getFrameAtTime方法的时间单位是微秒
    Bitmap bitmap = retriever.getFrameAtTime(pos * 1000);
    return bitmap;
}

若要从视频中截取一串时间相邻的画面,则可依据相邻的时间点调用getFrameAtTime方法,依次获得每帧位图再保存到存储卡。连续截取视频画面的示例代码如下:

// 获取视频文件中的图片帧列表。beginPos为毫秒时间,count为待获取的帧数量
public static List<String> getFrameList(Context ctx, Uri uri, int beginPos, int count) {
    String videoPath = uri.toString();
    String videoName = videoPath.substring(videoPath.lastIndexOf("/")+1);
    if (videoName.contains(".")) {
        videoName = videoName.substring(0, videoName.lastIndexOf("."));
    }
    List<String> pathList = new ArrayList<>();
    MediaMetadataRetriever retriever = new MediaMetadataRetriever();
    retriever.setDataSource(ctx, uri); // 将指定Uri设置为媒体数据源
    // 获得视频的播放时长
    String duration = retriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION);
    int dura_int = Integer.parseInt(duration)/1000;
    for (int i=0; i<dura_int-beginPos/1000 && i<count; i++) { // 最多只取前多少帧
        String path = String.format("%s/%s_%d.jpg",
                ctx.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString(), videoName, i);
        if (beginPos!=0 || !new File(path).exists()) {
            // 获取指定时间的帧图,注意getFrameAtTime方法的时间单位是微秒
            Bitmap frame = retriever.getFrameAtTime(beginPos*1000 + i*1000*1000);
            int ratio = frame.getWidth()/500+1;
            Bitmap small = BitmapUtil.getScaleBitmap(frame, 1.0/ratio);
            BitmapUtil.saveImage(path, small); // 把位图保存为图片文件
        }
        pathList.add(path);
    }
    return pathList;
}

运行测试该App,打开视频文件播放一阵后,点击“截取当前帧”按钮,可观察到截取结果如下图左侧所示;再点击“截取后九段”按钮,随后会跳转到各帧画面的列表项,成功截取到视频画面,如下图右侧所示。
在这里插入图片描述

增强摄制

本节介绍Android对相片和视频录制与播放的高级用法,内容包括如何使用增强CameraX库拍摄相片、如何使用增强的CameraX库录制视频、如何使用新型播放器ExoPlayer播放各类视频(网络视频和带字幕视频)。

使用CameraX拍照

Android的SDK一开始就自带了相机工具Camera,从Android 5.0开始又推出了升级版的Camera2,然而不管是初代的Camera还是二代的Camera2,编码过程都比较繁琐,对于新手而言有点艰深。为此谷歌公司再Jetpack库中集成了增强的相机库CameraX,想让相机编码(包括拍照和录像)变得更加方便。CameraX基于Camera2开发,它提供一致易用的API接口,还解决了设备兼容性问题,从而减少了编码工作量。
不管是拍照还是录像,都要在AndroidManifest.xml中添加相机权限,还要添加存储卡访问权限,代码如下:

<!-- 相机 -->
<uses-permission android:name="android.permission.CAMERA" />
<!-- 存储卡读写 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

由于CameraX来自Jetpack库,因此要修改模块build.gradle.kts,往dependencies节点添加以下几行配置,表示导入指定版本的CameraX库:

implementation ("androidx.camera:camera-core:1.0.2")
implementation ("androidx.camera:camera-camera2:1.0.2")
implementation ("androidx.camera:camera-lifecycle:1.0.2")
implementation ("androidx.camera:camera-view:1.0.0-alpha32")

使用CameraX拍照之前先要初始化相机,包括界面预览以及参数设定等,具体的初始化步骤说明如下:

  1. 准备一个预览视图对象PreviewView,并添加至当前界面。
  2. 获取相机提供器对象ProcessCameraProvider。
  3. 构建预览对象Preview,指定预览的宽高比例。
  4. 构建摄像头选择器对象CameraSelector,指定使用前置摄像头还是后置摄像头。
  5. 构建图像捕捉器对象ImageCapture,分别设置捕捉模式、旋转角度、宽高比例、闪光模式等拍照参数。
  6. 调用相机提供器对象的bindToLifecyccle方法,把相机选择器、预览视图、图像捕捉绑定到相机提供器。
  7. 调用预览视图对象的setSurfaceProvider方法,设置预览视图的表面提供器。

把上述的初始化步骤串起来,写到一个自定义的相机视图控件中,便形成了以下的CameraX初始化代码:

private Context mContext; // 声明一个上下文对象
private PreviewView mCameraPreview; // 声明一个预览视图对象
private CameraSelector mCameraSelector; // 声明一个摄像头选择器
private Preview mPreview; // 声明一个预览对象
private ProcessCameraProvider mCameraProvider; // 声明一个相机提供器
private ImageCapture mImageCapture; // 声明一个图像捕捉器
private VideoCapture mVideoCapture; // 声明一个视频捕捉器
private ExecutorService mExecutorService; // 声明一个线程池对象
private LifecycleOwner mOwner; // 声明一个生命周期拥有者
private int mCameraMode = MODE_PHOTO; // 0拍照,1录像
private int mCameraType = CameraSelector.LENS_FACING_BACK; // 摄像头类型,默认后置摄像头
private int mAspectRatio = AspectRatio.RATIO_16_9; // 宽高比例。RATIO_4_3表示宽高3比4;RATIO_16_9表示宽高9比16
private int mFlashMode = ImageCapture.FLASH_MODE_AUTO; // 闪光灯模式
private String mMediaDir; // 媒体保存目录

public CameraXView(Context context, AttributeSet attrs) {
    super(context, attrs);
    mContext = context;
    mCameraPreview = new PreviewView(mContext); // 创建一个预览视图
    ViewGroup.LayoutParams params = new ViewGroup.LayoutParams(
            ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT);
    mCameraPreview.setLayoutParams(params);
    addView(mCameraPreview); // 把预览视图添加到界面上
    mExecutorService = Executors.newSingleThreadExecutor(); // 创建一个单线程线程池
    mMediaDir = mContext.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS).toString();
}

// 打开相机
public void openCamera(LifecycleOwner owner, int cameraMode, OnStopListener sl) {
    mOwner = owner;
    mCameraMode = cameraMode;
    mStopListener = sl;
    mHandler.post(() ->  initCamera()); // 初始化相机
}

// 初始化相机
private void initCamera() {
    ListenableFuture future = ProcessCameraProvider.getInstance(mContext);
    future.addListener(() -> {
        try {
            mCameraProvider = (ProcessCameraProvider) future.get();
            resetCamera(); // 重置相机
        } catch (Exception e) {
            e.printStackTrace();
        }
    }, ContextCompat.getMainExecutor(mContext));
}

// 重置相机
private void resetCamera() {
    int rotation = mCameraPreview.getDisplay().getRotation();
    // 构建一个摄像头选择器
    mCameraSelector = new CameraSelector.Builder().requireLensFacing(mCameraType).build();
    // 构建一个预览对象
    mPreview = new Preview.Builder()
            .setTargetAspectRatio(mAspectRatio) // 设置宽高比例
            .build();
    // 构建一个图像捕捉器
    mImageCapture = new ImageCapture.Builder()
            .setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) // 设置捕捉模式
            .setTargetRotation(rotation) // 设置旋转角度
            .setTargetAspectRatio(mAspectRatio) // 设置宽高比例
            .setFlashMode(mFlashMode) // 设置闪光模式
            .build();
    if (mCameraMode == MODE_RECORD) { // 录像
        // 构建一个视频捕捉器
        mVideoCapture = new VideoCapture.Builder()
                .setTargetAspectRatio(mAspectRatio) // 设置宽高比例
                .setVideoFrameRate(60) // 设置视频帧率
                .setBitRate(3 * 1024 * 1024) // 设置比特率
                .setTargetRotation(rotation) // 设置旋转角度
                .setAudioRecordSource(MediaRecorder.AudioSource.MIC)
                .build();
    }
    bindCamera(MODE_PHOTO); // 绑定摄像头
    // 设置预览视图的表面提供器
    mPreview.setSurfaceProvider(mCameraPreview.getSurfaceProvider());
}

// 绑定摄像头
private void bindCamera(int captureMode) {
    mCameraProvider.unbindAll(); // 重新绑定前要先解绑
    try {
        if (captureMode == MODE_PHOTO) { // 拍照
            // 把相机选择器、预览视图、图像捕捉器绑定到相机提供器的生命周期
            Camera camera = mCameraProvider.bindToLifecycle(
                    mOwner, mCameraSelector, mPreview, mImageCapture);
        } else if (captureMode == MODE_RECORD) { // 录像
            // 把相机选择器、预览视图、视频捕捉器绑定到相机提供器的生命周期
            Camera camera = mCameraProvider.bindToLifecycle(
                    mOwner, mCameraSelector, mPreview, mVideoCapture);
        }
    } catch (Exception e) {
        e.printStackTrace();
    }
}

// 关闭相机
public void closeCamera() {
    mCameraProvider.unbindAll(); // 解绑相机提供器
    mExecutorService.shutdown(); // 关闭线程池
}

初始化相机后,即可调用图像捕捉器的takePicture方法拍摄照片了,拍照代码示例如下:

private String mPhotoPath; // 照片保存路径
// 获取照片的保存路径
public String getPhotoPath() {
    return mPhotoPath;
}

// 开始拍照
public void takePicture() {
    mPhotoPath = String.format("%s/%s.jpg", mMediaDir, DateUtil.getNowDateTime());
    ImageCapture.Metadata metadata = new ImageCapture.Metadata();
    // 构建图像捕捉器的输出选项
    ImageCapture.OutputFileOptions options = new ImageCapture.OutputFileOptions.Builder(new File(mPhotoPath))
            .setMetadata(metadata).build();
    // 执行拍照动作
    mImageCapture.takePicture(options, mExecutorService, new ImageCapture.OnImageSavedCallback() {
        @Override
        public void onImageSaved(ImageCapture.OutputFileResults outputFileResults) {
            BitmapUtil.notifyPhotoAlbum(mContext, mPhotoPath); // 通知相册来了张新图片
            mStopListener.onStop("已完成拍摄,照片保存路径为"+mPhotoPath);
        }

        @Override
        public void onError(ImageCaptureException exception) {
            mStopListener.onStop("拍摄失败,错误信息为:"+exception.getMessage());
        }
    });
}

然后在App代码中集成新定义的增强相机控件,先在布局文件中添加CameraXView节点,代码如下:

<com.example.chapter14.widget.CameraXView
    android:id="@+id/cxv_preview"
    android:layout_width="match_parent"
    android:layout_height="wrap_content" />

再给Java代码补充CameraXView对象的初始化以及拍照动作,其中关键代码示例如下:

private CameraXView cxv_preview; // 声明一个增强相机视图对象
private View v_black; // 声明一个视图对象
private ImageView iv_photo; // 声明一个图像视图对象
private final Handler mHandler = new Handler(Looper.myLooper()); // 声明一个处理器对象

// 初始化相机
private void initCamera() {
    // 打开增强相机,并指定停止拍照监听器
    cxv_preview.openCamera(this, CameraXView.MODE_PHOTO, (result) -> {
        runOnUiThread(() -> {
            iv_photo.setEnabled(true);
            Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
        });
    });
}

// 处理拍照动作
private void dealPhoto() {
    iv_photo.setEnabled(false);
    v_black.setVisibility(View.VISIBLE);
    cxv_preview.takePicture(); // 拍摄照片
    mHandler.postDelayed(() -> v_black.setVisibility(View.GONE), 500);
}

运行App,点击拍照图标,观察到增强相机的拍照效果如下图所示。其中,左图为准备拍照时的预览界面,右图为拍照结束后的观赏界面。
在这里插入图片描述

使用CameraX录像

要通过CameraX事先录像功能的话,初始化相机的步骤与拍照时大小异同,区别在于增加了对视频捕捉器VideoCapture的处理。需要修改的代码主要有三个地方,分别说明如下:

  1. 第一个地方是在build.gradle.kts里补充声明录音权限,完整的权限声明配置如下:
<!-- 相机 -->
<uses-permission android:name="android.permission.CAMERA" /> <
!-- 录音 -->
<uses-permission android:name="android.permission.RECORD_AUDIO" /> 
<!-- 存储卡读写 -->
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
  1. 第二个地方是在重置相机的resetCamera方法中,构建完图像捕捉器对象后,还要构建视频捕捉器对象,并设置视频的宽高比例、视频帧率、比特率(视频每秒录制的比特数)、旋转角度等录制参数。视频捕捉器的构建代码示例如下:
if (mCameraMode == MODE_RECORD) { // 录像
    // 构建一个视频捕捉器
    mVideoCapture = new VideoCapture.Builder()
            .setTargetAspectRatio(mAspectRatio) // 设置宽高比例
            .setVideoFrameRate(60) // 设置视频帧率
            .setBitRate(3 * 1024 * 1024) // 设置比特率
            .setTargetRotation(rotation) // 设置旋转角度
            .setAudioRecordSource(MediaRecorder.AudioSource.MIC)
            .build();
}
  1. 第三个地方是在绑定摄像头的bindCamera方法中,对于录像操作来说,需要把视频捕捉器绑定到相机提供器绑定到相机提供器的生命周期,而非绑定图像捕捉器。绑定视频捕捉器的代码示例如下:
// 把相机选择器、预览视图、图像捕捉器绑定到相机提供器的生命周期
Camera camera = mCameraProvider.bindToLifecycle(
        mOwner, mCameraSelector, mPreview, mImageCapture);

初始化相机之后,即可调用视频捕捉器的startRecording方法开始录像,或者调用stopRecording方法停止录像。录像代码如下:

private String mVideoPath; // 视频保存路径
private int MAX_RECORD_TIME = 15; // 最大录制时长,默认15秒
// 获取视频的保存路径
public String getVideoPath() {
    return mVideoPath;
}

// 开始录像
public void startRecord(int max_record_time) {
    MAX_RECORD_TIME = max_record_time;
    bindCamera(MODE_RECORD); // 绑定摄像头
    mVideoPath = String.format("%s/%s.mp4", mMediaDir, DateUtil.getNowDateTime());
    VideoCapture.Metadata metadata = new VideoCapture.Metadata();
    // 构建视频捕捉器的输出选项
    VideoCapture.OutputFileOptions options = new VideoCapture.OutputFileOptions.Builder(new File(mVideoPath))
            .setMetadata(metadata).build();
    // 开始录像动作
    mVideoCapture.startRecording(options, mExecutorService, new VideoCapture.OnVideoSavedCallback() {
        @Override
        public void onVideoSaved(VideoCapture.OutputFileResults outputFileResults) {
            mHandler.post(() -> bindCamera(MODE_PHOTO));
            mStopListener.onStop("录制完成的视频路径为"+mVideoPath);
        }

        @Override
        public void onError(int videoCaptureError, String message, Throwable cause) {
            mHandler.post(() -> bindCamera(MODE_PHOTO));
            mStopListener.onStop("录制失败,错误信息为:"+cause.getMessage());
        }
    });
    // 限定时长到达之后自动停止录像
    mHandler.postDelayed(() -> stopRecord(), MAX_RECORD_TIME*1000);
}

// 停止录像
public void stopRecord() {
    mVideoCapture.stopRecording(); // 视频捕捉器停止录像
}

当然,录像功能也要先在布局文件中添加CameraXView节点。为了方便观察当前已录制的时长,还可以在布局文件中添加计时器节点chronometer。接着给Java代码补充CameraXView对象的初始化以及录像动作,其中关键代码示例如下:

private CameraXView cxv_preview; // 声明一个增强相机视图对象
private Chronometer chr_cost; // 声明一个计时器对象
private ImageView iv_record; // 声明一个图像视图对象
private boolean isRecording = false; // 是否正在录像

// 初始化相机
private void initCamera() {
    // 打开增强相机,并指定停止录像监听器
    cxv_preview.openCamera(this, CameraXView.MODE_RECORD, (result) -> {
        runOnUiThread(() -> {
            chr_cost.setVisibility(View.GONE);
            chr_cost.stop(); // 停止计时
            iv_record.setImageResource(R.drawable.record_start);
            iv_record.setEnabled(true);
            isRecording = false;
            Toast.makeText(this, result, Toast.LENGTH_SHORT).show();
        });
    });
}

// 处理录像动作
private void dealRecord() {
    if (!isRecording) {
        iv_record.setImageResource(R.drawable.record_stop);
        cxv_preview.startRecord(15); // 开始录像
        chr_cost.setVisibility(View.VISIBLE);
        chr_cost.setBase(SystemClock.elapsedRealtime()); // 设置计时器的基准时间
        chr_cost.start(); // 开始计时
        isRecording = !isRecording;
    } else {
        iv_record.setEnabled(false);
        cxv_preview.stopRecord(); // 停止录像
    }
}

运行测试App,打开录像界面的初始效果如下图左图,此时除了预览画面外,界面下方还展示录制按钮。点击录制按钮录像,正在录像的界面如下右图所示,此时录制按钮换成了暂停按钮,其上方也跳动着已录制时长的数字。
在这里插入图片描述

新型播放器ExoPlayer

尽管录制视频的相机工具从经典相机Camera演进到了二代相机Camera2再到增强相机CameraX,然而播放视频仍是老控件MediaPlayer以及封装了MediaPlayer的视频视图,这个MediaPlayer用于播放本地的小视频还可以,如果用它播放网络视频就存在下列问题了:

  1. MediaPlayer不支持一边下载一边播放,必须等视频全部下载完才开始播放。
  2. MediaPlayer不支持视频直播协议,包括MPEG标准的自适应流(Dynamic Adaptive Streaming over HTTP, DASH)、苹果公司的直播流(HTTP Live Streaming, HLS)、微软公司的平滑流(Smooth Streaming)等。
  3. 未加密的视频容易被盗版,如果加密了,MediaPlayer反而无法播放加密视频。

为此Android在新一代的Jetppack库中推出了新型播放ExoPlayer,它的音视频内核依赖于原生的MediaCodec接口,不但能够播放MediaPlayer所支持的任意格式的视频,而且具备以下几点优异特性:

  1. 对于网络视频,允许一边下载一边播放。
  2. 支持三大视频直播协议,包括自适应流(DASH)、直播流(HLS)、平滑流(Smooth Streaming)。
  3. 只支持播放采取Widevine技术加密的网络视频。
  4. 只要提供了对应的字幕文件(srt格式),就支持在播放视频时同步显示字幕。
  5. 支持合并、串联、循环等多种播放方式。

Exoplayer居然能够做这么多事情,简直比MediaPlayer省心多了。当然,因为Exoplayer来自Jetpack库,所以使用之前要先修改build.gradle.kts,添加下面一行依赖配置:

implementation("com.google.android.exoplayer:exoplayer:2.19.1")

Exoplayer的播放界面采用播放器视图StylePlayerView,它的自定义属性分别说明如下:

  • show_buffering:缓冲进度的显示模式,值为never时表示从不显示,值为when_playing时表示在播放时显示,值为always时表示一直显示。
  • show_timeout:控制栏的消失间隔,单位为毫秒。
  • use_controller:是否显示控制栏,值为true时表示显示控制栏,值为false时表示不显示控制栏。
  • resize_mode:缩放模式。值为fit表示保持宽高比例缩放,值为fill表示填满播放器界面。

下面是布局文件中添加PlayerView节点的配置:

<com.google.android.exoplayer2.ui.StyledPlayerView
    android:id="@+id/pv_content"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:show_buffering="always"
    app:show_timeout="5000"
    app:use_controller="true"
    app:resize_mode="fit"/>

回到活动页面的代码,再调用播放器视图的setPlayer方法,设置已经创建好的播放器对象,然后才能让播放器进行播空操作。设置播放器的代码模板如下:

// 创建一个新型播放器对象
private ExoPlayer mPlayer = new ExoPlayer.Builder(this).build();
StyledPlayerView pv_content = findViewById(R.id.pv_content);
pv_content.setPlayer(mPlayer); // 设置播放器视图的播放器对象

以上代码把StyledPlayerView与ExoPlayer关联起来,后续的视频播放过程分成以下几个步骤:

  1. 创建指定视频格式的工厂对象。
  2. 创建指定URI地址的媒体对象MediaItem。
  3. 基于格式工厂和媒体对象创建媒体来源MediaSource。
  4. 设置播放器对象的媒体来源以及其他的播控操作。

其中步骤4的操作与ExoPlayer有关,它的常见方法分别说明如下:

  • setMediaSource:设置播放器的媒体来源。

  • addListener:给播放添加时间事件监听器。需要重写监听器接口Player.Listener的onPlaybackStateChanged方法,根据状态参数判断事件类型(取值见下表)。
    |Player类的播放状态| 说明 |
    |–|–|
    | STATE_BUFFERING | 视频正在缓冲 |
    | STATE_READY | 视频准备就绪 |
    | STATE_ENDED | 视频播放完毕 |

  • prepare:播放器准备就绪。

  • play:播放器开始播放。

  • seekTo:拖动当前进度到指定位置。

  • isPlaying:判断播放器是否正在播放。

  • getCurrentPosition:获得播放器当前的播放位置。

  • pause:播放器暂停播放。

  • stop:播放器停止播放。

  • release:释放播放器资源。

接下来把网络视频与本地视频的播放代码整合到一起,从工厂构建到开始播放的示例代码如下:

private ExoPlayer mPlayer; // 声明一个新型播放器对象
// 播放视频
private void playVideo(Uri uri) {
    DataSource.Factory factory = new DefaultDataSource.Factory(this);
    // 创建指定地址的媒体对象
    MediaItem videoItem = new MediaItem.Builder().setUri(uri).build();
    // 基于工厂对象和媒体对象创建媒体来源
    MediaSource videoSource = new ProgressiveMediaSource.Factory(factory)
            .createMediaSource(videoItem);
    mPlayer.setMediaSource(videoSource); // 设置播放器的媒体来源
    // 给播放器添加事件监听器
    mPlayer.addListener(new Player.Listener() {
        @Override
        public void onPlaybackStateChanged(int state) {
            if (state == Player.STATE_BUFFERING) { // 视频正在缓冲
                Log.d(TAG, "视频正在缓冲");
            } else if (state == Player.STATE_READY) { // 视频准备就绪
                Log.d(TAG, "视频准备就绪");
            } else if (state == Player.STATE_ENDED) { // 视频播放完毕
                Log.d(TAG, "视频播放完毕");
            }
        }
    });
    mPlayer.prepare(); // 播放器准备就绪
    mPlayer.play(); // 播放器开始播放
}

再举个播放带字幕的视频例子,此时除了构建视频文件的媒体来源,还需要构建字幕文件的媒体来源(字幕文件为srt格式),然后合并视频的媒体来源与字幕来源得到最终的媒体来源。包含字幕处理的播放器代码如下:

// 播放带字幕的视频
private void playVideoWithSubtitle(Uri videoUri, Uri subtitleUri) {
    Log.d(TAG, "getLanguage="+Locale.getDefault().getLanguage());
    // 创建HTTP在线视频的工厂对象
    DataSource.Factory factory = new DefaultDataSource.Factory(this);
    // 创建指定地址的媒体对象
    MediaItem videoItem = new MediaItem.Builder().setUri(videoUri).build();
    // 基于工厂对象和媒体对象创建媒体来源
    MediaSource videoSource = new ProgressiveMediaSource.Factory(factory)
            .createMediaSource(videoItem);
    // 语言要填null,否则中文会乱码。selectionFlags要填Format.NO_VALUE,否则看不到字幕
    // 创建指定地址的字幕对象。ExoPlayer只支持srt字幕,不支持ass字幕
    MediaItem.Subtitle subtitleItem = new MediaItem.Subtitle(subtitleUri,
            MimeTypes.APPLICATION_SUBRIP, null, Format.NO_VALUE);
    // 基于工厂对象和字幕对象创建字幕来源
    MediaSource subtitleSource = new SingleSampleMediaSource.Factory(factory)
            .createMediaSource(subtitleItem, C.TIME_UNSET);
    // 合并媒体来源与字幕来源
    MergingMediaSource mergingSource = new MergingMediaSource(videoSource, subtitleSource);
    mPlayer.setMediaSource(mergingSource); // 设置播放器的媒体来源
    mPlayer.prepare(); // 播放器准备就绪
    mPlayer.play(); // 播放器开始播放
}

运行测试该App,可观察到ExoPlayer的播放效果如下图所示。其中,左图为网络视频的播放界面,右图为带字幕视频的播放界面。
在这里插入图片描述

工程源码

文章涉及所有代码可点击工程源码下载。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1833933.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于GTX的64B66B编码IP生成(高速收发器二十)

点击进入高速收发器系列文章导航界面 1、配置GTX IP 相关参数 前文讲解了64B66B编码解码原理&#xff0c;以及GTX IP实现64B66B编解码的相关信号组成&#xff0c;本文生成64B66B编码的GTX IP。 首先如下图所示&#xff0c;需要对GTX共享逻辑进行设置&#xff0c;为了便于扩展&a…

Apple - Framework Programming Guide

本文翻译自&#xff1a;Framework Programming Guide&#xff08;更新日期&#xff1a;2013-09-17 https://developer.apple.com/library/archive/documentation/MacOSX/Conceptual/BPFrameworks/Frameworks.html#//apple_ref/doc/uid/10000183i 文章目录 一、框架编程指南简介…

文件操作(1)(C语言版)

前言&#xff1a; 为什么要学习文件操作&#xff1a; 1、如果大家写过一些代码&#xff0c;当运行结束的时候&#xff0c;这些运行结果将不复存在&#xff0c;除非&#xff0c;再次运行时这些结果才能展现在屏幕上面&#xff0c;就比如之前写过的通讯录。 现实中的通讯录可以保…

商超智能守护:AI监控技术在零售安全中的应用

结合思通数科大模型的图像处理、图像识别、目标检测和知识图谱技术&#xff0c;以下是详细的商超合规监测应用场景描述&#xff1a; 1. 员工仪容仪表监测&#xff1a; 利用图像识别技术&#xff0c;系统可以自动检测员工是否按照规范整理妆容、穿着工作服&#xff0c;以及是否…

Arm和高通的法律之争将扰乱人工智能驱动的PC浪潮

Arm和高通的法律之争将扰乱人工智能驱动的PC浪潮 科技行业高管和专家表示&#xff0c;两大科技巨头之间长达两年的法律大战可能会扰乱人工智能驱动的新一代个人电脑浪潮。 上周&#xff0c;来自微软(Microsoft)、华硕(Asus)、宏碁(Acer)、高通(Qualcomm)等公司的高管在台北举行…

Raylib的贪吃蛇

配置Raylib库 工具链主函数模板Draw: 绘制网格Snake: 初始化Draw&#xff1a;绘制蛇与果Input&#xff1a;移动Logic&#xff1a;游戏主要逻辑Draw: 游戏结束 工具链 mkdir snake cd snakeCMakeLists.txt cmake_minimum_required(VERSION 3.10) project(snake) set(CMAKE_EXP…

深度学习500问——Chapter11:迁移学习(3)

文章目录 11.3 迁移学习的常用方法 11.3.1 数据分布自适应 11.3.2 边缘分布自适应 11.3.3 条件分布自适应 11.3.4 联合分布自适应 11.3.5 概率分布自适应方法优劣性比较 11.3.6 特征选择 11.3.7 统计特征对齐方法 11.3 迁移学习的常用方法 11.3.1 数据分布自适应 数据分布自适…

libdrm 2.4.107 needed because amdgpu has the highest requirement

libdrm 2.4.107 needed because amdgpu has the highest requirement 1.问题分析解决 1.问题 Message: libdrm 2.4.107 needed because amdgpu has the highest requirement Run-time dependency libdrm_intel found: YES 2.4.107 Run-time dependency libdrm_amdgpu found: Y…

深度学习500问——Chapter11:迁移学习(4)

文章目录 11.3.8 流形学习方法 11.3.9 什么是finetune 11.3.10 finetune为什么有效 11.3.11 什么是网络自适应 11.3.12 GAN在迁移学习中的应用 参考文献 11.3.8 流形学习方法 什么是流行学习&#xff1f; 流行学习自从2000年在Science上被提出来以后&#xff0c;就成为了机器…

uniapp canvas生成海报

效果 封装组件&#xff0c;父组件 ref 调用 downImgUrl()函数&#xff0c;其他根据自己需求改 <template><view><view class"bgpart"><canvas class"canvas-wrap" canvas-id"canvasID" type"2d"></canvas…

41、基于深度学习的自适应线性预测(matlab)

1、原理及流程 自适应线性预测是一种基于递归最小二乘法&#xff08;Recursive Least Squares, RLS&#xff09;的线性预测方法&#xff0c;用于自适应地估计线性系统的参数。下面是自适应线性预测的原理和流程&#xff1a; 原理&#xff1a; 自适应线性预测的核心思想是通过…

Java | Leetcode Java题解之第162题寻找峰值

题目&#xff1a; 题解&#xff1a; class Solution {public int findPeakElement(int[] nums) {int n nums.length;int left 0, right n - 1, ans -1;while (left < right) {int mid (left right) / 2;if (compare(nums, mid - 1, mid) < 0 && compare(n…

转型AI产品经理(12):“希克定律”如何应用在Chatbot产品中

信息过载&#xff0c;这个对现代人来说是很常见的问题&#xff0c;信息获取变得越来越便捷的同时&#xff0c;也导致信息过载让我们无法及时做出有效决策&#xff0c;还可能演变成选择困难症。信息过载对用户体验的设计也有着显著的负面影响&#xff0c;如果我们的产品设计让人…

HTML静态网页成品作业(HTML+CSS)——美食火锅介绍网页(1个页面)

&#x1f389;不定期分享源码&#xff0c;关注不丢失哦 文章目录 一、作品介绍二、作品演示三、代码目录四、网站代码HTML部分代码 五、源码获取 一、作品介绍 &#x1f3f7;️本套采用HTMLCSS&#xff0c;未使用Javacsript代码&#xff0c;共有1个页面。 二、作品演示 三、代…

笔记 | 软件工程06-2:软件设计-软件体系结构设计

1 软件体系结构的概念 1.1 软件体系结构的设计元素 1.2 不同的抽象层次 1.3 软件体系结构的不同视图 1.3.1 软件体系结构的逻辑视图&#xff1a;包图 1.3.2 软件体系结构的逻辑视图&#xff1a;构件图 1.3.3 软件体系结构的开发视图 1.3.4 软件体系结构的部署视图 1.3.4.1 描述…

UV胶带和UV胶水的应用场景有哪些不同吗?

UV胶带和UV胶水的应用场景有哪些不同吗? UV胶带和UV胶水的应用场景确实存在不同之处&#xff0c;以下是详细的比较和归纳&#xff1a; 一&#xff1a;按使用场景来看&#xff1a; UV胶带的应用场景&#xff1a; 包装行业&#xff1a;UV胶带在包装行业中常用于食品包装、药…

About Apple Pay

本文翻译整理自&#xff1a;About Apple Pay &#xff08;更新时间&#xff1a;2017-03-16&#xff09; https://developer.apple.com/library/archive/ApplePay_Guide/index.html#//apple_ref/doc/uid/TP40014764 文章目录 一、关于 Apple Pay1、使用 Apple Pay2、测试 Apple …

Kaggle比赛:成人人口收入分类

拿到数据首先查看数据信息和描述 import pandas as pd import seaborn as sns import matplotlib.pyplot as plt # 加载数据&#xff08;保留原路径&#xff0c;但在实际应用中建议使用相对路径或环境变量&#xff09; data pd.read_csv(r"C:\Users\11794\Desk…

【机器学习300问】121、RNN是如何生成文本的?

当RNN模型训练好后&#xff0c;如何让他生成一个句子&#xff1f;其实就是一个RNN前向传播的过程。通常遵循以下的步骤。 &#xff08;1&#xff09;初始化 文本生成可以什么都不给&#xff0c;让他生成一首诗。首先&#xff0c;你需要确定采样的起始点。这可以是一个特殊的开…