0%

Android 详解音视频开发

1. Android 音视频开发

2. 怎样打开 Android 手机的开发者选项中的 USB 调试

  • Android 4.2 系统之前,设置、开发者选项、USB 调试
  • Android 4.2 系统之后,开发者选项默认是隐藏的,关于手机 –> 连续点击版本号 –> 开发者选项 –> USB 调试

3. 通知(Notification)使用的场景

  • Notification 既可以在 Activity 里创建,也可以在 BroadcastReceiver 里创建,还可以在 Service 里创建
  • 相比于 BroadcastReceiveService,在 Activity 里创建 Notification 的场景还是比较少的,因为一般只有当程序进入到后台的时候我们才需要使用 Notification

4. 写一个使用 Notification 的 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button sendNotice = (Button) findViewById(R.id.send_notice);
sendNotice.setOnClickListener(this);
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.send_notice:
// PendingIntent 和 Intent 都可以用于启动活动、启动服务以及发送广播
// 不同的是,Intent 更加倾向于去立即执行某个动作,而 PendingIntent 更加倾向于在某个合适的时机去执行某个动作
// 即可以把 PendingIntent 理解为延迟执行的 Intent
Intent intent = new Intent(this, NotificationActivity.class); // 点击通知启动另一个活动
PendingIntent pi = PendingIntent.getActivity(this, 0, intent, 0);

NotificationManager manager = (NotificationManager) getSystemService(NOTIFICATION_SERVICE);
Notification notification = new NotificationCompat.Builder(this) // 使用 support-v4 库中的 NotificationCompat 类来解决因为 Andoird 团队经常对通知功能的修改导致的 API 不稳定的问题
.setContentTitle("This is content title") // 指定通知的标题内容
.setContentText("This is content text") // 指定通知的正文内容
.setWhen(System.currentTimeMillis()) // 指定通知被创建的时间,这里指定的时间会显示在相应的通知上
.setSmallIcon(R.mipmap.ic_launcher) // 设置通知的小图标,这里只能使用纯 alpha 图层的图片进行设置,会显示在系统状态栏上
.setLargeIcon(BitmapFactory.decodeResource(getResources(), R.mipmap.ic_launcher)) // 设置通知的大图标,下拉状态栏时可以看到
.setContentIntent(pi) // 给通知增加点击功能
.setAutoCancel(true) // 点击通知后会自动取消。另一种方式是:manager.cancel(1),其中参数 1 正是 notify() 方法中的第一个参数
.setSound(Uri.fromFile(new File("/system/media/audio/ringtones/Luna.ogg"))) // 进阶技巧1:在通知发出时候播放一段音频
.setVibrate(new long[] { 0, 1000, 1000, 1000 }) // 进阶技巧2:在通知到来的时候让手机进行振动,注意此时需要在 AdroidManifest.xml 文件中声明权限:<uses-permission android:name="android.permissions.VIBRATE" />
.setLights(Color.GREEN, 1000, 1000) // 进阶技巧3:当通知到来时,如果手机又处于锁屏状态,则控制手机的前置 LED 灯使其闪烁。也可以调用 .setDefaults(NotificationCompat.DEFAULT_ALL) 使用默认效果,它会根据当前手机的环境来决定播放什么铃声,以及如何振动
.setStyle(new NotificationCompat.BigTextStyle().bigText("很长很长的文本")) // 高级功能1-1:让通知内容是长文本的富文本并完全显示,否则使用 setContentText() 的话,多余的内容会用省略号代替
.setStyle(new NotificationCompat.BitPictureStyle().bigPicture(BitmapFactory.decodeResource(getResource(), R.drawable.big_image))) // 高级功能1-2:让通知内容是富文本的大图
.setPriority(NotificationCompat.PRIORITY_MAX) // 高级功能2:设置通知的优先级。
// 这里一共有 5 个常量值可选,PRIORITY_DEFAULT 表示默认的优先级,和不设置的效果是一样的
// PRIORITY_MIN 表示最低的优先级,系统可能只会在特定的场景才显示这条通知,比如用户下拉状态栏的时候
// PRIORITY_LOW 表示较低的优先级,系统可能会将这类通知缩小,或改变其显示的顺序,将其排在优先级更高的通知之后
// PRIORITY_HIGH 表示较高的优先级,系统可能会将这类通知放大,或改变其显示顺序,将其排在比较靠前的位置
// PRIORITY_MAX 表示最高的优先级,这类通知必须要让用户立刻看到,甚至需要用户做出响应操作,此时的通知不是在系统状态栏显示一个小图标了,而是弹出了一个横幅,并附带了通知的详细内容,表示这是一条非常重要的通知,不管用户在玩游戏还是在看电影,这条通知都会显示在最上方 ,以此引起用户的注意。使用优先级最高的通知一定要谨慎,不能让用户反感
.build();
manager.notify(1, notification); // 第一个参数是 id,要保证为每个通知所指定的 id 都是不同的
break;
default:
break;
}
}
}

5. 写一个调用手机摄像头进行拍照的 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
public class MainActivity extends AppCompatActivity {
public static final int TAKE_PHOTO = 1;
private ImageView picture;
private Uri imageUri;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button takePhoto = (Button) findViewById(R.id.take_photo);
picture = (ImageView) findViewById(R.id.picture);
takePhoto.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 创建 File 对象,用于存储拍照后的图片
// 这里把 图片命名为 output_image.jgp,并把它存放在手机 SD 卡的应用关联缓存目录下
// 应用关联缓存目录是指 SD 卡中专门用于存放当前应用缓存数据的位置,调用 getExternalCache() 方法就可以得到这个目录,具体的路径是 /sdcard/Android/data/<package name>/cache
// 之所以要使用应用关联缓存目录来存放图片,是因为从 Android 6.0 系统开始,读写 SD 卡被列为了危险权限,如果将图片存放在 SD 卡的任何其他目录,都要进行运行时权限处理,而使用应用关联目录可以跳过运行时权限处理的这一步
File outputImage = new File(getExternalCacheDir(), "output_image.jpg");
try {
if(outputImage.exist()) {
outputImage.delete();
}
outputImage.createNewFile();
} catch(IOException e) {
e.printStackTrace();
}
if(Build.VERSION.SDK_INT >= 24) { //Android 7.0
// getUriForFile() 方法将 File 对象转换成一个封装过的 Uri 对象
// 之所以要进行这样一层转换,是因为从 Android 7.0 系统开始,直接使用本地真实路径的 Uri 被认为是不安全的,会抛出一个 FileUriExposedException 异常
// 而 FileProvider 则是一种特殊的内容提供器,它使用了和内容提供器类似的机制来对数据进行保护,可以选择性地将封装过的 Uri 共享给外部,从而提高了应用的安全性。此时,需要在 AndroidManifest.xml 文件中注册 FileProvider 这个内容提供器
imageUri = FileProvider.getUriForFile(MainActivity.this, "com.example.cameraalbumtest.fileprovider", outputImage);
} else {
// 这个 Uri 对象标识着 output_image.jgp 这张图片的本地真实路径
imageUri = Uri.fromFile(outputImage);
}
// 启动相机程序
Intent intent = new Intent("android.media.action.IMAGE_CAPTURE"); // 使用的是隐式 Intent,系统会找出能够响应这个 Intent 的活动去启动
intent.putExtra(MediaStore.EXTRA_OUTPUT, imageUri);
startActivityForResult(intent, TAKE_PHOTO);
}
});
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
case TAKE_PHOTO:
if(resultCode == RESULT_OK) {
try {
// 将拍摄的照片显示出来
Bitmap bitmap = BitmapFactory.decodeStream(getContentResolver().openInputStream(imageUri));
picture.setImageBitmap(bitmap);
} catch(FileNotFoundException e) {
e.printStackTrace();
}
}
break;
default:
break;
}
}
}

// AndroidManifest.xml 清单文件
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.cameraalbumtest">
// 在 Andrioid 4.4 系统之前,访问 SD 卡的应用关联目录也是要声明权限的,从 4.4 系统开始不再需要权限声明。这里为了兼容老版本系统的手机,还是声明一下访问 SD 卡的权限
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">
...
<provider
andorid:name="android.support.v4.content.FileProvider" // name 属性的值是固定的
android:authorities="com.example.cameraalbumtest.fileprovider" // authorities 属性的值必须要和上面 FileProvider.getUriForFile() 方法中的第二个参数一致
andorid:exported="false"
android:grantUriPermissions="true">
<meta-data // 指定 Uri 的共享路径,并引用了一个 @xml/file_paths 资源,这个资源需要手动创建
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" />
</provider>
</application>
</manifest>

// 在 res 目录中创建一个 xml 目录,里面创建一个 file_paths.xml 文件
<?xml version="1.0" encoding="utf-8"?>
<paths xmlns:android="http://schemas.android.com/apk/res/android">
<external-path name="my_images" path="" /> // 指定 Uri 共享的,name 属性值可以随便填,path 属性值表示共享的具体路径。这里设置空值表示将整个 SD 卡进行共享,当然也可以仅共享存放 output_image.jpg 这张图片的路径
</paths>

6. 写一个从相册中选择照片的 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
public class MainActivity extends AppCompatActivity {
...
public static final int CHOOSE_PHOTO = 2;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button takePhoto = (Button) findViewById(R.id.take_photo);
Button chooseFromAlbum = (Button) findViewById(R.id.choose_from_album);
...
chooseFromAlbum.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTENAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
// 相册中的照片都是存储在 SD 卡上的,要从 SD 卡中读取照片就需要申请这个权限。WRITE_EXTERNAL_STORAGE 表示同时授予 SD 卡读和写的能力
ActivityCompat.requestPermissions(MainActivity.this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);
} else {
openAlbum();
}
}
});
}

private void openAlbum() {
Intent intent = new Intent("android.intent.action.GET_CONTENT");
intent.setType("image/*");
startActivityForResult(intent, CHOOSE_PHOTO); // 打开相册
}

@Override
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
openAlbum();
} else {
Toast.makeText(this, "You denied the permission", Toast.LENGTH_SHORT).show();
}
break;
default:
break;
}
}

@Override
protected void onActivityResult(int requestCode, int resultCode, Intent data) {
switch (requestCode) {
...
case CHOOSE_PHOTO:
if(resultCode == RESULT_OK) {
// 判断手机系统版本号
// 因为Android 系统从 4.4 版本开始,选取相册中的图片不再返回图片真实的 Uri 了,而是一个封装过的 Uri,因此如果是 4.4 版本以上的手机就需要对这个 Uri 进行解析才行
if(Build.VERSION.SDK_INT >= 19) {
handleImageOnKitKat(data); // 4.4 及以上系统使用这个方法处理图片
} else {
handleImageBeforeKitKat(data); // 4.4 以下系统使用这个方法处理图片
}
}
break;
default:
break;
}
}

@TargetApi(19)
private void handleImageOnKitKat(Intent data) { // 解析封装过的 Uri
String imagePath = null;
Uri uri = data.getData();
if(DocumentContract.isDocumentUri(this, uri)) {
// 如果是 document 类型的 uri,则通过 document id 处理
String docId = DocumentsContract.getDocumentId(uri);
if("com.android.providers.media.documents".equals(uri.getAuthrity())) {
String id = docId.split(":")[1]; // 解析出数字格式的 id
String selection = MediaStore.Images.Media._ID + "=" + id;
imagePath = getImagePath(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, selection); // 获取图片的真实路径
} else if("com.android.provider.downloads.documents".equals(uri.getAuthority)) {
Uri contentUri = ContentUris.withAppendedId(Uri.parse("content://downloads/public_downloads"), Long.valueOf(docId));
imagePath = getImagePath(contentUri, null);
}
} else if("content".equalsIgnoreCase(uri.getScheme())) {
// 如果是 content 类型的 Uri,则使用普通方式处理
imagePath = getImagePath(uri, null);
} else if("file".equalsIgnoreCase(uri.getScheme())) {
// 如果是 file 类型的 Uri,直接获取图片路径即可
imagePath = uri.getPath();
}
displayImage(imagePath); // 根据图片路径显示图片
}

private void handleImageBeforeKitKat(Intent data) {
Uri uri = data.getData();
String imagePath = getImagePath(uri, null); // 不需要任何解析获取图片的真实路径
displayImage(imagePath);
}

private void String getImagePath(Uri uri, String selection) {
String path = null;
// 通过 Uri 和 selection 来获取真实的图片路径
Cursor cursor = getContentResolver().query(uri, null, selection, null, null);
if(cursor != null) {
if(cursor.moveToFirst()) {
path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
}
cursor.close();
}
return path;
}

private void displayImage(String imagePath) {
if(imagePath != null) {
Bitmap bitmap = BitmapFactory.decodeFile(imagePath);
picture.setImageBitmap(bitmap);
} else {
Toast.makeText(this, "failed to get image", Toast.LENGTH_SHORT).show();
}
}
}

// TODO: 上面的实现还不算完美,因为某些照片即使经过裁剪后体积仍然很大,直接加载到内存中有可能会导致程序崩溃。更好的做法是根据项目的需求先对照片进行适当的压缩,然后再加载到内存中

7. MediaPlayer 类中较为常用的控制方法有哪些

方法名 功能描述
setDataSource() 设置要播放的音频的位置
prepare() 在开始之前调用这个方法完成准备工作
start() 开始或继续播放音频
pause() 暂停播放音频
reset() 将 MP 对象重置到刚刚创建的状态
seekTo() 从指定的位置开始播放音频
stop() 停止播放音频,调用这个方法后的 MP 对象无法再播放音频
release() 释放掉与 MP 对象相关的资源
isPlaying() 判断当前 MP 对象是否正在播放音频
getDuration() 获取载入的音频文件的时长

8. 写一个播放音频的 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
// 首先,需要在 AndroidManifest.xml 文件中声明权限:<uses-permission android:name="android.pemission.WRITE_EXTERNAL_STORAGE" />
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private MediaPlayer mediaPlayer = new MediaPlayer();

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
Button play = (Button) findViewById(R.id.play);
Button pause = (Button) findViewByid(R.id.pause);
Button stop = (Button) findViewById(R.id.stop);
play.setOnClickListener(this);
pause.setOnClickListener(this);
stop.setOnClickListener(this);
// 动态申请 WRITE_EXTERNAL_STORAGE 权限,因为会在 SD 卡中放置一个音频文件,程序为了播放这个音频文件必须拥有访问 SD 卡的权限才行
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermission(MainActivity.this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORATE }, 1);
} else {
initMediaPlayer(); // 初始化 MediaPlayer
}
}

private void initMediaPlayer() {
try {
File file = new File(Environment.getExternalStorageDirectory(), "music.mp3");
mediaPlayer.setDataSource(file.getPath()); // 指定音频文件的路径
mediaPlayer.prepare(); // 让 MediaPlayer 进入到准备状态
} catch(Exception e) {
e.printStackTrace();
}
}

@Override
public void onRequestPermissionResult(int requestCode, String[] permissions, int[] grantResults) {
switch (requestCode) {
case 1:
if(grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
initMediaPlayer();
} else {
Toast.makeText(this, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
finish(); // 如果用户拒绝了权限申请,那么就调用 finish() 方法将程序直接关掉,因为如果没有 SD 卡的访问权限,这个程序什么也干不了
}
break;
default:
break;
}
}

@Override
public void onClick(View v) {
switch (v.getId() {
case R.id.play:
if(!mediaPlayer.isPlaying()) {
mediaPlayer.start(); // 开始播放
}
break;
case R.id.pause:
if(mediaPlayer.isPlaying()) {
mediaPlayer.pause(); // 暂停播放
}
break;
case R.id.stop:
if(mediaPlayer.isPlaying()) {
mediaPlayer.reset(); // 停止播放
initMediaPlayer();
}
break;
default:
break;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if(mediaPlayer != null) {
mediaPlayer.stop(); // 将与 MediaPlayer 相关的资源释放掉
mediaPlayer.release(); // 将与 MediaPlayer 相关的资源释放掉
}
}
}

9. VideoView 中常用的方法有哪些

方法名 功能描述
setVideoPath() 设置要播放的视频文件的位置
start() 开始或继续播放视频
pause() 暂停播放视频
resume() 将视频从头开始播放
seekTo() 从指定的位置开始播放视频
isPlaying() 判断当前是否正在播放视频
getDuration() 获取载入的视频文件的时长

10. 写一个播放视频的 Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 首先,需要在 AndroidManifest.xml 文件中声明权限:<uses-permission android:name="android.pemission.WRITE_EXTERNAL_STORAGE" />
public class MainActivity extends AppCompatActivity implements View.OnClickListener {
private VideoView videoView;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
videoView = (VideoView) findViewById(R.id.video_view);
Button play = (Button) findViewById(R.id.play);
Button pause = (Button) findViewById(R.id.pause);
Button replay = (Button) findViewById(R.id.replay);
play.setOnClickListener(this);
pause.setOnClickListener(this);
replay.setOnClickListener(this);
// 因为你视频文件会放在 SD 卡上,所以需要动态申请权限
if(ContextCompat.checkSelfPermission(MainActivity.this, Manifest.permission.WRITE_EXTERNAL_STOREAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(MainActivity.this, new String[] { Manifest.permission.WRITE_EXTERNAL_STORAGE }, 1);
} else {
initVideoPath(); // 初始化 VideoView
}
}

private void initVideoPath() {
File file = new File(Environment.getExternalStorageDirectory(), "movie.mp4");
videoView.setVideoPath(file.getPath()); // 指定视频文件的路径
}

@Override
public void onRequestPermissionResult(int requestCod, String[] permissions, int[] grantResults) {
switch (requestCode) {
case 1:
if(grantResult.length > 0 && grantResults[0] == PackageManage.PERMISSION_GRANTED) {
initVideoPath();
} else {
Toast.makeText(ths, "拒绝权限将无法使用程序", Toast.LENGTH_SHORT).show();
finish();
}
break;
default:
break;
}
}

@Override
public void onClick(View v) {
switch (v.getId()) {
case R.id.play:
if(!videoView.isPlaying()) {
videoView.start(); // 开始播放
}
break;
case R.id.pause:
if(videoView.isPlaying()) {
videoView.pause(); // 暂停播放
}
break;
case R.id.replay:
if(videoView.isPlayint()) {
videoView.resume(); // 从头播放
}
break;
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if(videoView != null) {
videoView.suspend(); // 将 VideoView 所占用的资源释放掉
}
}
}

11. 怎样理解 VideoView(场景、优缺点、底层原理)

  • VideoView 的底层仍然是使用 MediaPlayer 来对文件进行控制的,只不过 VideoView 做了一层很好的封装
  • VideoView 并不是一个万能的视频播放工具类,它在视频格式的支持以及播放效率方面都存在着较大的不足
  • 所以,如果想要仅仅使用 VideoView 就编写出一个功能非常强大的视频播放器是不太现实的。但是如果只是用于播放一些游戏的片头动画,或者某个应用的视频宣传,使用 VideoView 还是绰绰有余的
-------------------- 本文结束感谢您的阅读 --------------------