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

2024-06-19 09:52

本文主要是介绍安卓多媒体(音频录播、传统摄制、增强摄制),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

本章介绍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(); // 获得录制好的音频uriString 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"><Buttonandroid:id="@+id/btn_choose"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="打开相册播放视频"android:textColor="@color/black"android:textSize="17sp" /><VideoViewandroid: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; // 声明一个视频视图对象@Overrideprotected 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() {@Overridepublic void onImageSaved(ImageCapture.OutputFileResults outputFileResults) {BitmapUtil.notifyPhotoAlbum(mContext, mPhotoPath); // 通知相册来了张新图片mStopListener.onStop("已完成拍摄,照片保存路径为"+mPhotoPath);}@Overridepublic void onError(ImageCaptureException exception) {mStopListener.onStop("拍摄失败,错误信息为:"+exception.getMessage());}});
}

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

<com.example.chapter14.widget.CameraXViewandroid: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() {@Overridepublic void onVideoSaved(VideoCapture.OutputFileResults outputFileResults) {mHandler.post(() -> bindCamera(MODE_PHOTO));mStopListener.onStop("录制完成的视频路径为"+mVideoPath);}@Overridepublic 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.StyledPlayerViewandroid: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() {@Overridepublic 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.chinasem.cn/article/1074692

相关文章

安卓链接正常显示,ios#符被转义%23导致链接访问404

原因分析: url中含有特殊字符 中文未编码 都有可能导致URL转换失败,所以需要对url编码处理  如下: guard let allowUrl = webUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) else {return} 后面发现当url中有#号时,会被误伤转义为%23,导致链接无法访问

消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法

消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法   消除安卓SDK更新时的“https://dl-ssl.google.com refused”异常的方法 [转载]原地址:http://blog.csdn.net/x605940745/article/details/17911115 消除SDK更新时的“

安卓玩机工具------小米工具箱扩展工具 小米机型功能拓展

小米工具箱扩展版                     小米工具箱扩展版 iO_Box_Mi_Ext是由@晨钟酱开发的一款适用于小米(MIUI)、多亲(2、2Pro)、多看(多看电纸书)的多功能工具箱。该工具所有功能均可以免root实现,使用前,请打开开发者选项中的“USB调试”  功能特点 【小米工具箱】 1:冻结MIUI全家桶,隐藏状态栏图标,修改下拉通知栏图块数量;冻结

安卓开发板_联发科MTK开发评估套件串口调试

串口调试 如果正在进行lk(little kernel ) 或内核开发,USB 串口适配器( USB 转串口 TTL 适配器的简称)对于检查系统启动日志非常有用,特别是在没有图形桌面显示的情况下。 1.选购适配器 常用的许多 USB 转串口的适配器,按芯片来分,有以下几种: CH340PL2303CP2104FT232 一般来说,采用 CH340 芯片的适配器,性能比较稳定,价

安卓实现弹出软键盘屏幕自适应调整

今天,我通过尝试诸多方法,最终实现了软键盘弹出屏幕的自适应。      其实,一开始我想通过EditText的事件来实现,后来发现,安卓自带的函数十分强大,只需几行代码,便可实现。实现如下:     在Manifest中设置activity的属性:android:windowSoftInputMode="adjustUnspecified|stateHidden|adjustResi

深入探讨生成对抗网络(GANs):颠覆传统的AI创作方式

在人工智能的快速发展中,生成对抗网络(Generative Adversarial Networks, GANs)无疑是一个引人注目的技术。自2014年由Ian Goodfellow等人首次提出以来,GANs已经在图像生成、文本生成、视频生成等多个领域展现出了惊人的能力。本文将详细解析GANs的原理、结构以及应用场景,帮助读者全面理解这一颠覆性的技术。 一、GANs的基本原理 生成对抗网络(G

黑神话:悟空》增加草地绘制距离MOD使游戏场景看起来更加广阔与自然,增强了游戏的沉浸式体验

《黑神话:悟空》增加草地绘制距离MOD为玩家提供了一种全新的视觉体验,通过扩展游戏中草地的绘制距离,增加了场景的深度和真实感。该MOD通过增加草地的绘制距离,使游戏场景看起来更加广阔与自然,增强了游戏的沉浸式体验。 增加草地绘制距离MOD安装 1、在%userprofile%AppDataLocalb1SavedConfigWindows目录下找到Engine.ini文件。 2、使用记事本编辑

生日贺卡录放音芯片,多段音频录音ic生产厂商,NVF04M-32minute

可以录音播放的生日贺卡与传统的纸质贺卡相比,它有着创意以及个性的特点,仅需少量的电子元器件,即可实现录音功能,搭配上文字,让声音存储在生日贺卡里,让贺卡也变得有温度,祝福我想亲口对TA说。 生日贺卡录放音芯片方案——NVF04M 采用外挂SPI存储器的方式,达到录音的功能。它的主要特点是声音清晰,录音时间长。目前可以提供的录音时间为32分钟。NV04FM的一个显著特点是存储时间灵活,客

Pr:首选项 - 音频硬件

Pr菜单:编辑/首选项 Edit/Preferences Premiere Pro 首选项中的“音频硬件” Audio Hardware选项卡可以指定计算机的音频设备和设置,还可以指定 Pr 用于音频回放和录制的 ASIO 和 MME 设置(仅限 Windows)或 CoreAudio 设置(仅限 macOS)。 当连接音频硬件设备时,该类型设备的硬件设置(如默认输入、默认输出、主时钟、延迟和