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

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

相关文章

探索蓝牙协议的奥秘:用ESP32实现高质量蓝牙音频传输

蓝牙(Bluetooth)是一种短距离无线通信技术,广泛应用于各种电子设备之间的数据传输。自1994年由爱立信公司首次提出以来,蓝牙技术已经经历了多个版本的更新和改进。本文将详细介绍蓝牙协议,并通过一个具体的项目——使用ESP32实现蓝牙音频传输,来展示蓝牙协议的实际应用及其优点。 蓝牙协议概述 蓝牙协议栈 蓝牙协议栈是蓝牙技术的核心,定义了蓝牙设备之间如何进行通信。蓝牙协议

潜艇伟伟迷杂交版植物大战僵尸2024最新免费安卓+ios苹果+iPad分享

嗨,亲爱的游戏迷们!今天我要给你们种草一个超有趣的游戏——植物大战僵尸杂交版。这款游戏不仅继承了原有经典游戏的核心玩法,还加入了许多创新元素,让玩家能够体验到前所未有的乐趣。快来跟随我一起探索这个神奇的世界吧! 植物大战僵尸杂交版最新绿色版下载链接: https://pan.quark.cn/s/d60ed6e4791c 🔥 创新与经典的完美结合 植物大战僵尸杂交版在保持了原游戏经典玩

二本毕业,我是如何成为BAT-安卓开发工程师?

1.对基础原理不断挖掘 进入公司,我的职位是Linux应用开发工程师,做App网络传输模块,本质上就是把本地的数据通过socket传输到服务端。用到的技术是C语言,网络编程,多线程编程。 那时是最痛苦的几个月,因为非计算机出生,很多东西都不会,经常程序段错误,都不能定位到问题代码。 当时处于试用期间,秉承着不成功则成仁的心态开始恶补相关的基础知识以及代码规范。那时网络上没有现成的视频资料,艰

常用上网增强类Chrome扩展

Chrome是个非常好用的浏览器,拥有丰富的扩展资源库,能够满足网民各种各样的需求,对于网民来说,通过Chrome扩展来增强上网体验是一个基本需求,但是安装过多的扩展有容易耗费大量系统资源,今天就给大量挑选一些常用的上网增强类Chrome扩展,供大家参考。   LastPass:用于管理大量网站的密码,给不同网站设置不同的密码,支持自动登录,支持手机两步验证。建议在普通和隐身模式下都启用这个扩展

2021-02-16物料档案条码添加和蓝牙条码标签打印,金蝶安卓盘点机PDA,金蝶仓库条码管理WMS系统

物料档案条码添加和蓝牙条码标签打印,金蝶安卓盘点机PDA https://member.bilibili.com/platform/upload-manager/article 本期视频我们来讲解一下汉点机PDA条码添加和条码标签蓝牙便携打印: 在实际使用中,我们商品有两种情况: 一种是商品本身就有条码, 比如:超市卖的可口可乐,牛奶等商品,商品本身就有69开头的国标码,那么我们就可以使用盘点

仓库盘点好方法,使用安卓盘点机PDA扫描商品条码进行超市盘点

仓库管理我们为什么要盘点? 因为传统的进销存出入库都需要电脑一行行的人工手工录单,比如入库时,人眼识别这个商品是什么商品,然后电脑上选择该商品,录入数量。人眼识别要求入库人对商品非常熟悉,而且对于包装规格相近的很容易弄错,张冠李戴,A商品的录单时记录成为B商品了。所以人工手工录单效率低,误差大,是导致我们进销存管理软件中帐面库存存跟仓库门店实际库存不相符合的主要原因。电脑账存跟实际库存不符合,所

高通安卓12-安卓系统定制2

将开机动画打包到system.img里面 在目录device->qcom下面 有lito和qssi两个文件夹 现在通过QSSI的方式创建开机动画,LITO方式是一样的 首先加入自己的开机动画,制作过程看前面的部分 打开qssi.mk文件,在文件的最后加入内容 PRODUCT_COPY_FILES += $(LOCAL_PATH)/bootanimation.zip:$(TA

macbook配置adb环境和用adb操作安卓手机

(参考:ADB工具包的安装与使用_adb工具箱-CSDN博客) 第一步:从Android开发者网站下载Android SDK(软件开发工具包)。下载地址为: 第二步:解压下载的SDK压缩文件到某个目录中。 进入解压后的目录,找到其中的"platform-tools"文件夹。记录"platform-tools"文件夹路径: 第三步:将"platform-tools"文件夹的路径添加到系

终极解决方案,传统极速方案,下载软件的双雄对决!

在数字资源日益丰富的今天,下载管理器成为了我们日常生活中不可或缺的工具。市场上两款备受欢迎的下载管理软件——Internet Download Manager(IDM)和迅雷11,它们以各自的特色和优势,满足了不同用户群体的需求。 软件连接:都是绿色版本!极速10+MB/S,下载软件的双雄对决! 传统极速方案(迅雷11绿色版) 迅雷11免安装版以其轻量级和便携性,为用户带来了全新的下载体验。

颠覆传统编程:用ChatGPT十倍提升生产力

我们即将见证一个新的时代!这是最好的时代,也是最坏的时代! 需求背景 背景: 平时会编写博客,并且会把这个博客上传到github上,然后自己买一个域名挂到github上。 我平时编写的博客会有一些图片来辅助说明的,写完之后如果我把图片和文字全部都上传到博客网站,后期图片很多时就会导致网站加载特别慢 所以想把图片存储在一个公共的对象存储平台(腾讯云的cos服务),这样只要上传一