1
0
杨充 4 жил өмнө
parent
commit
5f530d0d25

+ 4 - 4
Demo/src/main/AndroidManifest.xml

@@ -18,7 +18,7 @@
         android:networkSecurityConfig="@xml/network_security_config"
         android:theme="@style/AppTheme"
         tools:ignore="GoogleAppIndexingWarning">
-        <activity android:name="com.yc.ycvideoplayer.MainActivity"
+        <activity android:name="com.yc.ycvideoplayer.newPlayer.activity.TypeActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:screenOrientation="portrait">
             <intent-filter>
@@ -62,9 +62,9 @@
         <activity android:name="com.yc.ycvideoplayer.newPlayer.surface.TestSurfaceActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:screenOrientation="portrait"/>
-        <activity android:name="com.yc.ycvideoplayer.newPlayer.activity.TypeActivity"
-            android:configChanges="orientation|keyboardHidden|screenSize"
-            android:screenOrientation="portrait"/>
+<!--        <activity android:name="com.yc.ycvideoplayer.newPlayer.activity.TypeActivity"-->
+<!--            android:configChanges="orientation|keyboardHidden|screenSize"-->
+<!--            android:screenOrientation="portrait"/>-->
         <activity android:name="com.yc.ycvideoplayer.newPlayer.activity.NormalActivity"
             android:configChanges="orientation|keyboardHidden|screenSize"
             android:screenOrientation="portrait"/>

+ 169 - 0
read/32.音频焦点抢占问题.md

@@ -0,0 +1,169 @@
+#### **目录介绍**
+- **1.为什么要处理音频焦点问题**
+- 1.1 发现问题说明
+- 1.2 为什么要处理音频焦点问题
+- **2.处理音频焦点逻辑**
+- 2.1 伪代码逻辑思路
+- 2.2 请求和放弃音频焦点
+- 2.3 当音频焦点发生变化处理逻辑
+- 2.4 focusChange参数值
+- **3.关于其他说明**
+- 3.1 版本更新情况
+- 3.2 个人博客
+
+
+###  0.备注
+- 建议结合代码,看博客更加高效,项目地址:https://github.com/yangchong211/
+- [博客大汇总,持续更新目录说明,记录所有开源项目和博客](http://www.jianshu.com/p/53017c3fc75d)
+- 关于本项目地址:https://github.com/yangchong211/YCAudioPlayer
+
+
+### 1.为什么要处理音频焦点问题
+#### 1.1 发现问题说明
+- 如果手机上安装了两个音频播放器,当一个正在播放的时候,打开第二个播放歌曲,有没有发现第一个自动暂停了……
+- 如果你在听音频的同时,又去打开了其它视频APP,你会发现音频APP暂停播放了……
+- 如果你正在听音频或者看视频时,来电话了,那么音视频便会暂停。挂了电话后音乐又继续播放,视频则需要点击按钮播放,是不是很奇怪
+- 当你收到消息,比如微信消息,并且有消息声音的时候,那么听音频的那一瞬间,音频的声音会变小了,然后过会儿又恢复了。是不是很有意思。
+- 别蒙圈,这个就叫做音频捕获和丢弃焦点。
+- **至于如何处理,可以看我的源码案例:** https://github.com/yangchong211/YCAudioPlayer
+- **还可以看我的博客,潇湘剑雨,博客大汇总:** https://www.jianshu.com/p/53017c3fc75d
+
+#### 1.2 为什么要处理音频焦点问题
+- 如果不处理捕获与丢弃音频焦点的话,那么同时开几个音视频播放器,就会出现多个声音。那样会很嘈杂,一般线上的APP都会做这个处理,不过一些GitHub案例demo中一般没处理。为了协调设备的音频输出,android提出了Audio Focus机机制,获取audio focus必须调用AudioManager的requestAudioFocus()方法。
+
+
+### 2.处理音频焦点逻辑
+#### 2.1 伪代码逻辑思路
+- 简单来说,就是这三步逻辑方法
+- 在service的oncreate方法中调用初始化方法
+- 在播放音频的时候开始请求捕获音频焦点
+- 在音频销毁的时候开始丢弃音频焦点
+
+
+#### 2.2 请求和放弃音频焦点
+- **2.2.1 失去焦点有三种类型**
+- 1.失去短暂焦点
+- 2.失去永久焦点
+- 3.Ducking
+
+- **2.2.2 失去焦点原理说明**
+- 当重新获得焦点的时候,如果通话结束,恢复播放;获取音量并且恢复音量。这个情景应该经常遇到。
+- 当永久丢失焦点,比如同时打开播放器,则停止或者暂停播放,否则出现两个声音
+- 当短暂丢失焦点,比如比如来了电话或者微信视频音频聊天等等,则暂停或者停止播放
+- 当瞬间丢失焦点,比如手机来了通知。前提是你的通知是震动或者声音时,会短暂地将音量减小一半。当然你也可以减小三分之一,哈哈!
+
+
+- **2.2.1 首先获取AudioManager对象**
+```
+ mAudioManager = (AudioManager) content.getSystemService(AUDIO_SERVICE);
+```
+
+- **2.2.2 请求和放弃音频焦点**
+- AudioFocus这个其实是音频焦点,一般情况下音乐播放器都会处理这个音频焦点的,在其丢失音频焦点的情况会将音频暂停或者停止的逻辑的,等到再次获取到音频焦点的情况下会再次恢复播放的。
+- 音频获取焦点可以通过requestAudioFocus()方法获得,在音频焦点成功获取后,该方法会返回AUDIOFOCUS_REQUEST_GRANTED常量,否则,会返回AUDIOFOCUS_REQUEST_FAILED常量。
+- 音频失去焦点abandonAudioFocus()方法,这会通知系统您的App不再需要音频焦点,并移除相关OnAudioFocusChangeListener的注册。如果释放的是短暂音调焦点,那么被打断的音频会被继续播放。
+- **代码如下所示**
+
+```
+/**
+ * 请求音频焦点,开始播放时候调用
+ * @return
+ */
+public boolean requestAudioFocus() {
+	return mAudioManager.requestAudioFocus(this, AudioManager.STREAM_MUSIC,
+			AudioManager.AUDIOFOCUS_GAIN) == AudioManager.AUDIOFOCUS_REQUEST_GRANTED;
+}
+
+/**
+ * 放弃音频焦点,销毁播放时候调用
+ */
+public void abandonAudioFocus() {
+	mAudioManager.abandonAudioFocus(this);
+}
+```
+
+#### 2.3 当音频焦点发生变化处理逻辑
+- 当焦点发生变化的时候,可以在这个方法onAudioFocusChange中处理业务逻辑
+- 详细案例,可以直接参考我的demo,地址:https://github.com/yangchong211/YCAudioPlayer
+
+```
+/**
+ * 当音频焦点发生变化的时候调用这个方法,在这里可以处理逻辑
+ * 欢迎访问我的GitHub:https://github.com/yangchong211
+ * 如果可以的话,请star吧
+ * @param focusChange       焦点改变
+ */
+@Override
+public void onAudioFocusChange(int focusChange) {
+	int volume;
+	switch (focusChange) {
+		// 重新获得焦点
+		case AudioManager.AUDIOFOCUS_GAIN:
+			if (!willPlay() && isPausedByFocusLossTransient) {
+				// 通话结束,恢复播放
+				mPlayService.playPause();
+			}
+			//获取音量
+			volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+			if (mVolumeWhenFocusLossTransientCanDuck > 0 && volume ==
+					mVolumeWhenFocusLossTransientCanDuck / 2) {
+				// 恢复音量
+				mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
+						mVolumeWhenFocusLossTransientCanDuck, AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
+			}
+
+			isPausedByFocusLossTransient = false;
+			mVolumeWhenFocusLossTransientCanDuck = 0;
+			break;
+		// 永久丢失焦点,如被其他播放器抢占
+		case AudioManager.AUDIOFOCUS_LOSS:
+			if (willPlay()) {
+				forceStop();
+			}
+			break;
+		// 短暂丢失焦点,比如来了电话或者微信视频音频聊天等等
+		case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT:
+			if (willPlay()) {
+				forceStop();
+				isPausedByFocusLossTransient = true;
+			}
+			break;
+		// 瞬间丢失焦点,如通知
+		case AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK:
+			// 音量减小为一半
+			volume = mAudioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
+			if (willPlay() && volume > 0) {
+				mVolumeWhenFocusLossTransientCanDuck = volume;
+				mAudioManager.setStreamVolume(AudioManager.STREAM_MUSIC,
+						mVolumeWhenFocusLossTransientCanDuck / 2,
+						AudioManager.FLAG_REMOVE_SOUND_AND_VIBRATE);
+			}
+			break;
+		default:
+			break;
+	}
+}
+```
+
+#### 2.4 focusChange参数值
+- 1. AUDIOFOCUS_GAIN:获取audio focus
+- 2. AUDIOFOCUS_LOSS:失去audio focus很长一段时间,必须停止所有的audio播放,清理资源
+- 3. AUDIOFOCUS_ LOSS_TRANSIENT:暂时失去audio focus,但是很快就会重新获得,在此状态应该暂停所有音频播放,但是不能清除资源
+- 4. AUDIOFOCUS_ LOSS_TRANSIENT _CAN_DUCK:暂时失去 audio focus,但是允许持续播放音频(以很小的声音),不需要完全停止播放。
+
+
+
+### 3.关于其他说明
+#### 3.1 版本更新情况
+- v1.0.0 17年12月28日
+- v1.0.1 18年3月6日
+
+#### 3.2 个人博客
+- **github:** [https://github.com/yangchong211](https://github.com/yangchong211)
+- **知乎:** [https://www.zhihu.com/people/yang-chong-69-24/pins/posts](https://www.zhihu.com/people/yang-chong-69-24/pins/posts)
+- **简书:** [http://www.jianshu.com/u/b7b2c6ed9284](http://www.jianshu.com/u/b7b2c6ed9284)
+- **csdn:** [http://my.csdn.net/m0_37700275](http://my.csdn.net/m0_37700275)
+- **喜马拉雅听书:** [http://www.ximalaya.com/zhubo/71989305/](http://www.ximalaya.com/zhubo/71989305/)
+- 泡在网上的日子:[http://www.jcodecraeer.com/member/content_list.php?channelid=1](http://www.jcodecraeer.com/member/content_list.php?channelid=1)
+- 邮箱:yangchong211@163.com
+- 阿里云博客:[https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV](https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV)

+ 376 - 0
read/40.完整音频播放器分析.md

@@ -0,0 +1,376 @@
+#### **目录介绍**
+- **0.关于项目介绍**
+- 0.1 **本案例关联博客[共14篇,系统性介绍,全面展开]**
+- 0.2 本案例其他说明
+- 0.3 全面系统化学习音视频播放器
+- **1.关于音频播放器基本功能**
+- 1.1 基本实现的功能
+- 1.2 音频的缓存,下载,播放权限等功能
+- 1.3 音频后台播放功能
+    * 1.3.1 音频播放可以支持后台播放
+    * 1.3.2 Android系统有自动回收内存机制
+- 1.4 需要注意的问题
+    * 1.4.1 一般播放音频APP具有的功能
+    * 1.4.2 注意的问题有哪些呢?[共11条,持续找问题中]
+- 1.5 待实现功能
+- **2.关于音频播放器高级功能**
+- 2.1 能够自由扫描本地音频[掌握]
+    * 2.1.1 在activity中扫描还是在service扫描
+    * 2.1.2 扫描音乐的逻辑
+- 2.2 锁屏联动媒体播放器[重点]
+    * 2.2.1 什么是锁屏联动媒体播放器
+    * 2.2.2 如何实现,逻辑思路
+    * 2.2.3 注意要点分析
+- 2.3 捕获/丢弃音乐焦点处理[重点]
+    * 2.3.1 发现有趣的焦点问题
+    * 2.3.2 为什么要处理音频焦点问题
+    * 2.3.3 具体的代码逻辑
+    * 2.3.4 当焦点变化时,我的处理逻辑
+- 2.4 耳机拔出时暂停播放[理解]
+    * 2.4.1 拔出耳机自动暂停 , 插入耳机自动恢复播放
+    * 2.4.2 实现的原理分析
+    * 2.4.3 蓝牙耳机又是如何实现[求赐教]
+- 2.5 耳机线控,耳机控制声音[理解]
+    * 2.5.1 耳机按键也可以控制音量调节
+    * 2.5.2 5.0之前和5.0之后比较
+    * 2.5.3 实现的逻辑分析
+- 2.6 滑动通知栏可以关闭程序[掌握]
+- 2.7 外放,耳机,听筒之间的切换[掌握]
+- **3.关于项目架构的技术堆栈**
+- 3.1 该项目App整体架构
+- 3.2 主要的技术要点
+- 3.3 日志打印,选择数据库
+- 3.4 主要的开源框架介绍
+- 3.5 项目代码规范,UI说明
+- 3.6 技术难点
+- **4.关于音频编码解码**
+- 4.0 认识音频基础属性
+- 4.1 音频编码
+- 4.2 音频解码
+- **5.关于音频其他知识点**
+- 5.1 如何给音频加密
+- 5.2 Android提供3套音频播放的API,如何选择
+- 5.3 如何设置音视频播放速率
+- 5.4 关于通知栏控制音频播放逻辑处理
+- **6.参考案例和博客链接**
+- 6.1 参考的项目
+- 6.2 参考的博客
+- 6.3 参考的市面上App
+- **7.关于此项目更新日志**
+- **8.关于其他介绍**
+
+
+### 0.关于项目介绍
+#### 0.1 本案例关联博客[共13篇]
+> **1.关于音频基础博客**
+>
+- 0.0.1 [如何扫描本地音频或者视频](http://www.jcodecraeer.com/plus/view.php?aid=9305)
+- 0.0.2 [深入学习酷狗,混沌大学那种锁屏页面原理](http://blog.csdn.net/m0_37700275/article/details/79262793)
+- 0.0.3 [为什么要捕获/丢弃音频焦点详细讲解](http://blog.csdn.net/m0_37700275/article/details/79269250)
+- 0.0.4 [耳机声控,以及耳机拔出或者插入控制播放暂停](http://blog.csdn.net/m0_37700275/article/details/79269722)
+- 0.0.5 [音视频编码解码深入分析总结](http://www.jcodecraeer.com/plus/view.php?aid=9334)
+- 0.0.6 [实现音视频1.5倍,2倍播放速率](http://www.jcodecraeer.com/plus/view.php?aid=9313)
+- 0.0.7 [音频的加密与解密案例实践深入总结](http://www.jcodecraeer.com/plus/view.php?aid=9331)
+- 0.0.8 [音频基础知识和概念的介绍](http://www.jcodecraeer.com/plus/view.php?aid=9322)
+- 0.0.9 [音视频混合案例介绍]后期更新
+- 0.1.0 [如何构建知识图谱,**重点**](https://zhuanlan.zhihu.com/p/33563691)
+- 0.1.1 [完整视频播放器案例总结与分析](http://www.jcodecraeer.com/plus/view.php?aid=9147)
+- 0.1.2 [关于博客笔记大汇总](https://www.jianshu.com/p/53017c3fc75d)
+- 0.1.3 [Android编码规范](https://my.oschina.net/zbj1618/blog/1620101)
+- 0.1.4 [Notification通知控制播放](http://www.jcodecraeer.com/plus/view.php?aid=9482)
+
+
+#### 0.2 本案例其他说明
+- 持续更新中,看了网上许多博客,许多写的很好,但是没有对应的案例感觉有时难以下手。于是拿来主义,一边写案例,一边写笔记,也算是相结合吧。可能会存在一些问题,欢迎指出,谢谢!
+- 该项目对应的地址链接:https://github.com/yangchong211/YCAudioPlayer
+- 直接看该案例中音乐模块,其他部分还没有完善,音频播放这块大都完成。
+- 感谢大神前辈们的开源项目,尤其是remusic,StylishMusicPlayer,Music-Player,ListenerMusicPlayer,PonyMusic等案例。
+- 对于remusic等项目,大神之作,但是代码文档少,注释少,要向入门感觉有些困难。但是对于我这个播放器,注释详细,还有配套文档博客,方便初期看,要向更上一层楼,还是看大神之作。结合实际情况阅读代码很重要!!!
+- 如果可以,欢迎star!!
+
+
+#### 0.3 全面系统化学习音视频播放器
+> **1.全面介绍的好处**
+>
+- 0.3.1 构建自己的知识框架
+- 0.3.2 案例与博客相互结合,以解决问题为目的地学习或者阅读更加有利于提高自己的实战技能
+
+
+
+### 1.关于音频播放器基本功能
+#### 1.1 基本实现的功能
+
+> **A基础功能**
+> 
+- 1.1.1 基础的音频播放功能有:播放,暂停,下一首,上一首
+- 1.1.2 播放监听,播放完了自动下一首;滑动监听,拖动SeekBar可以控制播放进度,进度条显示播放进度功能
+- 1.1.3 设置音量控制监听,手机上音量滑动监听,手机按键控制音量,还有耳机控制音量。注意要同步!
+- 1.1.4 播放类型:顺序循环播放,随机循环播放,单曲循环播放等等
+- 1.1.5 播放进度快慢设置:参考混沌大学,播放速度可以设置为1.0x,1.5x,2.0x
+- 1.1.6 播放快进快退,参考混沌大学,间隔时间是15秒
+- 1.1.7 可通过媒体按钮和 Notification 通知栏来控制媒体播放
+- 1.1.8 停止播放后,可通过滑动移除 Notification 来关闭应用,这个是参考喜马拉雅APP,因为平时玩的比较多,所以个人感觉这个功能还是不错的。我的喜马拉雅:http://www.ximalaya.com/71989305/profile/
+
+
+#### 1.2 音频的缓存,下载,播放权限等功能
+> **B其他功能**
+> 
+- 1.2.1 音频可以边播放变缓存
+- 1.2.2 支持下载到本地,如果有付费音频,还需要转码加密
+- 1.2.3 支持设置播放权限
+
+
+#### 1.3 音频后台播放功能
+> **C其他功能**
+> 
+- **1.3.1 音频播放可以支持后台播放**
+- 当切换到后台时,显示通知栏,可以通过通知栏来控制上一首,下一首,播放暂停功能,主要需要保证播放进度和音频属性数据信息同步
+- **1.3.2 Android系统有自动回收内存机制**
+- 如果系统内存紧张,就会触发该机制,应用就有可能被回收,不过Android提供了前台机制,比如当音频播放器切换到后台时,这个时候可以通过通知栏中按钮,点击切换音乐,那么当播放时启动前台机制,而暂停时取消前台机制。**保证内存不足时也不会回收该应用**。
+
+
+#### 1.4 需要注意的问题
+> **D注意问题**
+> 
+- **1.4.1 一般播放音频APP具有的功能:**
+- 播放、暂停、切换歌曲、进度调节、切换播放模式、专辑封面显示、音频列表、音频管理,添加收藏,下载等功能。(由于国产手机大多都是修改过的Android系统,因此系统自带播放器功能也不一样,这里以Android原生播放器为参考,同时结合了喜马拉雅,混沌大学,得到等音频付费APP)
+- **1.4.2 注意的问题有哪些呢?**
+- a.如手机来电时,音频需要自动暂停播放
+- b.耳机拔出时,同样需要暂停
+- c.Android有些手机耳机有音控键,有些没有,但还是要支持耳机线控
+- d.当在播放音频的时候,又去打开别的视频软件,这个时候音频声音会变小,有的是暂停。等到关闭其他音视频软件时,点击播放,声音如何调整
+- e.对于付费的音频,如何加密播放了?两种情况:1,如果没有下载则可以通过返回接口字段控制权限;2.如果下载了,那么如何才能实现只有自己的播放器才能播放音频,而其他的不能呢?
+- f.参考了混沌大学,对于音频列表,有的显示是下载完成,有的是未下载,有的是下载中,要区分好状态。
+- g.参考了喜马拉雅,不管是否有网络,都可以直接扫描本地喜马拉雅音频文件。那么对于音频,有十几种格式,如何区别咱们APP音频与其他音频。
+- h.我看到比如喜马拉雅下载的音频中还有排序的功能,比如根据文件大小,或者根据文件名首字母排序。该如何实现呢?
+- 还在探索中
+- 详细更多内容可以看我的笔记:[关于博客笔记大汇总](https://www.jianshu.com/p/53017c3fc75d)
+
+
+#### 1.5 待实现功能
+- 1.5.1 在应用被销毁后,可以通过耳机线控来重新启动
+- 1.5.2 如何自定义均衡器配置,部分有该功能,可参考喜马拉雅
+- **1.5.3 参考混沌大学,将视频内容分离成视频和音频两个部分,当播放音频时可以切换成播放视频,当播放视频时可以切换成音频,还能够记录播放位置。这个如何实现了?思考???欢迎同行给出建议,注意接口返回的音频和视频是不同的接口,只是内容一样而已。**
+- 1.5.4 外放,耳机,听筒之间的切换。关于这块内容可以直接看本篇博客:
+
+
+### 2.关于音频播放器高级功能
+#### 2.1 能够自由扫描本地音频
+- **2.1.1在activity中扫描还是在service扫描**
+- 写音频案例之前,我也参考了部分小的音频播放器案例,我发现有些直接写在activity中,当然这个最后结果是也能正常展示扫描的音频。如果音频少,那么没什么影响;如果音频多,几百首,那么activity中直接处理有点不妥当了,会卡顿。所以我最后还是想在service中处理扫描逻辑
+- **2.1.2 扫描音乐的逻辑**
+- 一般音频APP中会有我的下载或缓冲页面,那么扫描本地音频。而扫描一般通过ContentProvider配合Media相关类查询系统数据库,获得媒体库中的歌曲信息。
+- 通过阅读扫描音乐代码可知,是在服务中扫描音乐。但是有个问题,如果是又下载了新的音频,怎样才能做到及时更新呢?还是直接重新扫描?
+- 这个可以直接参考我的案例代码:util包--musicUtils--FileScanManager中的代码
+- 关于扫描本地音乐的逻辑,可以看这篇博客:[如何扫描本地音频或者视频](http://www.jcodecraeer.com/plus/view.php?aid=9305)
+
+
+
+#### 2.2 联动系统媒体播放器
+- **2.2.1 什么是锁屏联动媒体播放器**
+- **播放器除了播放了音乐之外什么都没做,就可以分别在任务管理、锁屏、负一屏控制播放器。**
+- 也可以这样通俗的解释,这个举例子说一个应用场景,我使用混沌大学听音频,然后我关闭了屏幕(屏幕灭了),当我再次打开的时候,屏幕的锁屏页面或者顶层页面便会出现一层音频播放器控制的页面,那么即使我不用解锁屏幕,也照样可以控制音频播放器的基本播放操作。如果你细心观察一下,也会发现有些APP正式这样操作的。目前我发现QQ音乐,混沌大学等是这样的
+- **2.2.2 如何实现,逻辑思路**
+- 第一步:在服务中注册屏幕熄灭广播
+- 第二步:处理逻辑,发现屏幕熄灭就开启锁屏页面,再次点亮屏幕时就可以看到锁屏页面
+- 第三步:点击锁屏页面上的按钮,比如上一首,下一首,播放暂停可以与主程序同步信息。
+- 第四步:滑动锁屏页面,锁屏页面被销毁,进入程序主界面。
+
+
+#### 2.3 捕获/丢弃音频焦点处理
+- **2.3.1 这个问题细心点就会发现,很有趣。**
+- 如果手机上安装了两个音频播放器,当一个正在播放的时候,打开第二个播放歌曲,有没有发现第一个自动暂停了……
+- 如果你在听音频的同时,又去打开了其它视频APP,你会发现音频APP暂停播放了……
+- 如果你正在听音频或者看视频时,来电话了,那么音视频便会暂停。挂了电话后音乐又继续播放,视频则需要点击按钮播放,是不是很奇怪
+- 当你收到消息,比如微信消息,并且有消息声音的时候,那么听音频的那一瞬间,音频的声音会变小了,然后过会儿又恢复了。是不是很有意思。
+- 别蒙圈,这个就叫做音频捕获和丢弃焦点。
+- **至于如何处理,可以看我的源码案例:** https://github.com/yangchong211/YCAudioPlayer
+- **还可以看我的博客,潇湘剑雨,博客大汇总:** https://www.jianshu.com/p/53017c3fc75d
+- **2.3.2 为什么要处理音频焦点问题**
+- 如果不处理捕获与丢弃音频焦点的话,那么同时开几个音视频播放器,就会出现多个声音。那样会很嘈杂,一般线上的APP都会做这个处理,不过一些GitHub案例demo中一般没处理。
+- **2.3.3 具体的代码逻辑可以直接参考代码案例**
+- 简单来说,就是这三步逻辑方法
+- 2.3.3.1 在service的oncreate方法中调用初始化方法
+- 2.3.3.2 在播放音频的时候开始请求捕获音频焦点
+- 2.3.3.3 在音频销毁的时候开始丢弃音频焦点
+- **2.3.4 当焦点变化时,我的处理逻辑**
+- 2.3.4.1 当重新获得焦点的时候,如果通话结束,恢复播放;获取音量并且恢复音量。这个情景应该经常遇到。
+- 2.3.4.2 当永久丢失焦点,比如同时打开播放器,则停止或者暂停播放,否则出现两个声音
+- 2.3.4.3 当短暂丢失焦点,比如比如来了电话或者微信视频音频聊天等等,则暂停或者停止播放
+- 2.3.4.4 当瞬间丢失焦点,比如手机来了通知。前提是你的通知是震动或者声音时,会短暂地将音量减小一半。当然你也可以减小三分之一,哈哈!
+
+
+#### 2.4 耳机拔出时暂停播放
+- **2.4.1 拔出耳机自动暂停 , 插入耳机自动恢复播放**
+- 在使用音频APP时,细心的你有没有发现,拔出耳机,暂停播放了;插上耳机又恢复播放了。是不是很神奇……
+- 如何实现这个功能了,这个我也是通过百度才知道了,但是代码还是不太懂,IntentFilter作用?后来明白,其实不用深入底层原理也没有多大关系……
+- **2.4.2 实现的原理分析**
+- 其原理还是通过发广播接收者控制播放与暂停功能。首先创建一个广播接收者,然后在播放时注册,在暂停时取消,就可以实现这个功能。
+- 具体的逻辑可以直接参考代码……其实这个也是百度查的!
+
+
+#### 2.5 耳机线控,耳机控制声音
+- **2.5.1 耳机按键也可以控制音量调节**
+- 不得不说Android手机需要考虑不同情况,就拿耳机来说,有的有音控,有的没有音控,有的手机支持,有的手机不支持,虽然说不太重要,但还是可以思考一下。目前参考大量的案例,只能解决大部分的正常控制声音功能。
+- 刚开始看到这个真是蒙圈了,不过有Google,不担心。对于程序员来说没有实现不了的功能,只有自己的技术过不过关,哈哈,同事说的……找到了答案
+- **2.5.2 5.0之前和5.0之后比较**
+- 对于5.0以上系统的手机,激活了MediaSession,就可以不用关心耳机声控了,会自己实现。
+- 对于5.0以前系统的手机,还是需要自己手动设置相关属性的,需要自己手动实现
+- **2.5.3 实现的逻辑分析**
+- 这里暂且只是讨论5.0以前的处理方法,需要自己监听耳机按键
+- 通过代码KeyEvent event = intent.getParcelableExtra(Intent.EXTRA_KEY_EVENT);获取到KeyEvent的对象,然后获取对应点击事件的code,然后再做播放暂停,上一首,下一首处理。具体代码可以参考EarphoneControlReceiver类
+
+
+
+#### 2.7 外放,耳机,听筒之间的切换[掌握]
+- 可以直接看这篇博客:[耳机声控,以及耳机拔出或者插入控制播放暂停](http://blog.csdn.net/m0_37700275/article/details/79269722)
+
+
+### 3.关于项目架构的技术堆栈
+#### 3.1 该项目App介绍
+- **3.1.1 bug管理**
+- 可以使用腾讯免费的bugly管理平台,便于跟踪处理bug,有数据分析
+- **3.1.2 测试部分**
+- 自己测试,一般来说自己不容易发现自己的问题,哪怕有些问题指出来后很简单。就像以前读书考试,自己检查试卷,也没检查出什么问题,但就是错了许多……
+- 基本的monkey测试,可以测试app在快速点击下崩溃问题
+- 运用平台测试,基于经费,还是选择免费的平台,一般应用市场也会有测试分析,比如华为测试,腾讯测试就很不错。
+
+
+#### 3.2 主要的技术要点
+- 3.2.1 音频播放的基础功能,播放暂停,上一首下一首,快进快退,播放状态切换,音量调节等等。
+- 3.2.2 音频播放前后台切换时,避免内存不足而回收资源问题
+- 3.2.3 支持音频下载,收藏,删除,缓存等功能。后期添加。
+- 3.2.4 支持耳机线控播放,耳机拔出自动暂停,支持耳机按钮控制音频播放音量
+- 3.2.5 支持播放详情页面,Notification通知,还有底部播放控制部分音频数据同步。本项目中采用是接口监听实现activity,fragment,service之间的通信
+- 3.2.6 支持添加类似酷狗音乐那种的锁屏页面
+- 3.2.7 支持来电或者拔出耳机时暂停播放的逻辑
+
+
+#### 3.3 日志打印,选择数据库
+- 3.3.1 日志打印,对于线上项目要规范,分为测试和正式打包日志。上线后要关掉部分打印日志
+- 3.3.2 关于数据库存储,这个看具体情况,使用原生SQLite或者Realm数据库
+- 3.3.3 由于之前开发都是使用Realm数据库,所以暂且使用它吧
+
+
+#### 3.4 主要的开源框架介绍
+- 注明:对于开源框架的选择,能够满足自己需求下,选择体量小的,并且相对成熟的的开源库
+- 3.4.1 网络请求框架
+- 3.4.2 gson解析框架
+- 3.4.3 H5网页可以用腾讯X5浏览器框架
+- 3.4.4 图片加载框架,谷歌glide,或者square/picasso
+- 3.4.5 音视频播放器底层框架ijkplayer
+- 3.4.6 下载框架FileDownloader
+- 3.4.7 自定义开源库:参考自己的GitHub项目
+- 3.5.8 自定义支持上拉加载更多,下拉刷新,可以自定义头部和底部,可以添加多个headAdapter,使用一个原生recyclerView就可以搞定复杂界面。地址:https://github.com/yangchong211/YCRefreshView
+- 3.5.9 其他库可以直接看GitHub
+
+
+#### 3.5 项目代码规范,UI说明
+- 3.5.1 项目的包名规范,分包说明,都要有文档
+- 3.5.2 关于注释,类注释,方法注释,还有常量注释可以参考阿里出版的java规范文档,通过插件可以自动检测注释规范。要求有输出文档。可以参考第三方sdk文档规范,清晰,简单而又能够让人快速看懂
+- 3.5.3 类名,方法名,常量或者变量等名称,还有布局文件xml中的各种空间id,都要采用统一的命名方式。要求见者知意……
+- 3.5.4 关于color颜色,string字符串,需要统一规范
+- 3.5.5 添加了捕获/丢弃音频焦点处理逻辑,可以监听两个以上音频播放时,暂停与播放逻辑
+- 3.5.6 关于代码规范,可以看这篇博客:[Android编码规范](https://my.oschina.net/zbj1618/blog/1620101)
+- 3.5.7 或者直接看阿里出的java手册也行……写代码,不仅方便自己看懂,也要让别人看懂!
+
+
+#### 3.6 技术难点
+- 当屏幕从灭了到亮了,会出现锁屏页面,那么锁屏页面如何做到和播放器其他音乐信息同步?
+- 耳机声控,以及抢占焦点处理逻辑
+- 进度条没有正确监听,滑动到某一位置,歌曲会从头播放
+- 通知栏,音频播放详情页,主页面底部音频控制栏,还有锁屏页面音频信息同步。保持进度同步。
+- 项目中,视频播放和音频播放来回切换,不同步。
+- 其他待整理。
+
+### 4.关于音频编码解码
+- **4.0 认识音频基础属性**
+- 采样频率(Sample Rate):每秒采集声音的数量,它用赫兹(Hz)来表示。(采样率越高越靠近原声音的波形)
+- 采样精度(Bit Depth):指记录声音的动态范围,它以位(Bit)为单位。(声音的幅度差)
+- 声音通道(Channel):声道数。比如左声道右声道。
+- **4.1 音频编码与解码**
+- 可以直接看这篇博客:[音视频编码解码深入分析总结](http://www.jcodecraeer.com/plus/view.php?aid=9334)
+
+
+
+### 5.关于音频其他知识点
+#### 5.1 如何给音频加密解密
+- **还在探索中**
+- 业务场景分析:当用户下载了我们的视频,或者音频,那么在下载完成后需要对下载的文件进行加密。以避免使用其它播放器软件直接可以观看或者听下载的音视频……
+- 仅仅改变音视频的格式,或者不用后缀名的办法不可取。目前从咱们APP下载的视频,虽然没有后缀名,但是使用同类型的视频播放器照样可以打开。
+
+#### 5.2 Android提供3套音频播放的API,如何选择
+- **Android提供3套音频播放的API,分别是:MediaPlayer,SoundPool,AudioTrack**
+- MediaPlayer 更加适合在后台长时间播放本地音乐文件或者在线的流式资源;
+- SoundPool 则适合播放比较短的音频片段,比如游戏声音、按键声、铃声片段等等,并且文件的大小限制在1M左右,可支持多个文件同时播放。SoundPool提供了pause和stop方法,但这些方法建议最好不要轻易使用,因为有些时候它们可能会使你的程序莫名其妙的终止。调用pause并不会立即停止,而是把缓冲区里的数据播放完才会停下来,也许会多播放一秒钟。 
+- AudioTrack 则更接近底层,提供了非常强大的控制能力,支持低延迟播放,适合流媒体和VoIP语音电话等场景。
+- **所以MediaPlayer是更加适合于第三方的播放器的,但是如果某些播放器用的并不是MediaPlayer那我也没有办法了,不在考虑的范围内。而本项目也是使用MediaPlayer的……**
+
+
+#### 5.4 关于通知栏控制音频播放逻辑处理
+- 我们可以发现许多音频类的APP都会有通知栏控制播放逻辑这个功能,那么本案例也有该功能。具体可以看代码!
+- 关于这块更详细的内容,可以直接看这篇博客: [Notification通知控制播放](http://www.jcodecraeer.com/plus/view.php?aid=9482)
+
+
+### 6.参考案例和博客链接
+#### 6.1 参考的项目
+- 注意:如果是带着问题去看项目,去直接瞄准某个功能,感觉学习效率更高一些。先实现功能,后再思考原理或技巧。
+- https://github.com/ocwvar/DarkPurple
+- https://github.com/andremion/Music-Player
+- https://github.com/googlesamples/android-UniversalMusicPlayer
+- https://github.com/DuanJiaNing/Musicoco
+- https://github.com/ryanhoo/StylishMusicPlayer
+- https://github.com/hefuyicoder/ListenerMusicPlayer
+- https://github.com/cpacm/MoeMusic
+
+
+#### 6.2 参考的博客
+- 博客链接如下所示:
+- FFmpeg的库函数源代码分析文章列表:http://blog.csdn.net/leixiaohua1020/article/details/12677129
+- 多媒体编解码基础知识:http://zzqhost.github.io/hostwiki/%E5%A4%9A%E5%AA%92%E4%BD%93%E7%9B%B8%E5%85%B3_%E5%A4%9A%E5%AA%92%E4%BD%93%E7%BC%96%E8%A7%A3%E7%A0%81%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86.html#toc_1.1.2
+- Android 注册媒体按键监听:https://www.jianshu.com/p/b52754c50f89
+- Android 多媒体控制 来电监听-耳机插拔监听-耳机按钮监听:http://blog.csdn.net/ocwvar/article/details/53107005
+- Android音频焦点详解:https://www.jianshu.com/p/e5785dcba952
+- Android 音频技术开发总结:https://yq.aliyun.com/articles/8637
+- [译]Android音频: 如何使用AudioTrack播放一个WAV格式文件?:http://blog.csdn.net/langwang2/article/details/50189977
+- 利用MediaExtractor和MediaCodec实现音频编解码和混音:http://yedaxia.me/Android-MediaExtractor-And-MediaCodec/
+- Android中播放音频的几种方式:http://blog.csdn.net/u013366008/article/details/76577372/
+
+
+- **6.2.3 关于锁屏页面的博客**
+- 直接参考锁屏页面这篇博客:[深入学习酷狗,混沌大学那种锁屏页面原理](http://blog.csdn.net/m0_37700275/article/details/79262793)
+
+
+#### 6.3 参考的市面上App
+- 喜马拉雅,得到,QQ音乐,混沌大学,等等。这些都是十分牛的项目,当然许多功能都在模仿与不断修正中
+- 关于主要页面的示意图
+- ![image](http://upload-images.jianshu.io/upload_images/4432347-321ca95b07ceaa6d.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
+![image](http://upload-images.jianshu.io/upload_images/4432347-ff0234e301c0018f.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
+![image](http://upload-images.jianshu.io/upload_images/4432347-d79a02ff20308fc6.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
+![image](http://upload-images.jianshu.io/upload_images/4432347-12af346217b1b2c5.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
+
+
+### 7.关于此项目更新日志
+- 7.1 v1.0.0 更新于2017年11月5日
+- 7.2 v1.1.0 更新于2018年1月29日
+- 7.3 v1.1.1 更新于2018年1月30日
+- 7.4 v1.2.0 更新于2018年3月5日
+
+
+### 8.关于其他介绍
+#### 8.1 个人博客
+- **github:** [https://github.com/yangchong211](https://github.com/yangchong211)
+- **知乎:** [https://www.zhihu.com/people/yang-chong-69-24/pins/posts](https://www.zhihu.com/people/yang-chong-69-24/pins/posts)
+- **简书:** [http://www.jianshu.com/u/b7b2c6ed9284](http://www.jianshu.com/u/b7b2c6ed9284)
+- **csdn:** [http://my.csdn.net/m0_37700275](http://my.csdn.net/m0_37700275)
+- **喜马拉雅听书:** [http://www.ximalaya.com/zhubo/71989305/](http://www.ximalaya.com/zhubo/71989305/)
+- 泡在网上的日子:[http://www.jcodecraeer.com/member/content_list.php?channelid=1](http://www.jcodecraeer.com/member/content_list.php?channelid=1)
+- 邮箱:yangchong211@163.com
+- 阿里云博客:[https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV](https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV)
+
+
+
+
+
+
+

+ 263 - 0
read/42.音视频本地文件扫描.md

@@ -0,0 +1,263 @@
+#### **目录介绍**
+- **1.关于Content Providers的作用**
+- 1.1 Content Providers作用介绍
+- 1.2 利用ContentResolver.query()读取数据
+- 1.3 将cursor中的数据转存到事先定义好的List中
+- 1.4 关于实体类介绍
+- 1.5 注意添加权限,还需要动态申请读取本地文件权限
+- **2.如何展示缩略图,歌手,专辑等信息**
+- 2.1 获取缩略图
+- **3.关于其他介绍**
+- 3.1 关于音视频案例和博客汇总介绍
+- 3.2 关于版本更新情况
+- 3.3 关于我的博客
+
+
+###  0.备注
+- 建议结合代码,看博客更加高效,项目地址:https://github.com/yangchong211/
+- [博客大汇总,持续更新目录说明,记录所有开源项目和博客](http://www.jianshu.com/p/53017c3fc75d)
+
+
+### 1.关于Content Providers的作用
+#### 1.1 Content Providers作用介绍
+- Android通过Content Providers将公共数据类型(音频、视频、图像、联系人信息等)整合到数据库中,储存地址和基本信息。
+
+#### 1.2 利用ContentResolver.query()读取数据
+- **1.2.1 这里举的例子以搜索手机中音频为案例**
+
+```
+public final Cursor query (Uri uri,                 //Url,即查询路径
+                String[] projection,   //查询时希望获得的列,如果填null,则返回所有列
+                String selection,      //查询时的条件,select语句中where用到,可填null
+                String[] selectionArgs, //查询条件属性值
+                String sortOrder,       //查询到的数据的默认排序,null则不进行排序
+                )
+```
+
+- **1.2.2 实际案例中的代码展示**
+
+```
+Cursor cursor = context.getContentResolver().query(
+                MediaStore.Audio.Media.EXTERNAL_CONTENT_URI,
+                new String[]{
+                        BaseColumns._ID,
+                        MediaStore.Audio.AudioColumns.IS_MUSIC,
+                        MediaStore.Audio.AudioColumns.TITLE,
+                        MediaStore.Audio.AudioColumns.ARTIST,
+                        MediaStore.Audio.AudioColumns.ALBUM,
+                        MediaStore.Audio.AudioColumns.ALBUM_ID,
+                        MediaStore.Audio.AudioColumns.DATA,
+                        MediaStore.Audio.AudioColumns.DISPLAY_NAME,
+                        MediaStore.Audio.AudioColumns.SIZE,
+                        MediaStore.Audio.AudioColumns.DURATION
+                },
+                SELECTION,
+                new String[]{String.valueOf(filterSize), String.valueOf(filterTime)},
+                MediaStore.Audio.Media.DEFAULT_SORT_ORDER);
+```
+
+
+#### 1.3 将cursor中的数据转存到事先定义好的List中
+- 1.3.1 代码展示如下所示
+
+```
+List<LocalMusic> musicList = new ArrayList<>();
+if (cursor == null) {
+	return musicList;
+}
+
+int i = 0;
+while (cursor.moveToNext()) {
+	// 是否为音乐,魅族手机上始终为0
+	int isMusic = cursor.getInt(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.IS_MUSIC));
+	if (!isFly() && isMusic == 0) {
+		continue;
+	}
+
+	long id = cursor.getLong(cursor.getColumnIndex(BaseColumns._ID));
+	String title = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.TITLE)));
+	String artist = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ARTIST));
+	String album = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM)));
+	long albumId = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.ALBUM_ID));
+	long duration = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.DURATION));
+	String path = cursor.getString(cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DATA));
+	String fileName = cursor.getString((cursor.getColumnIndex(MediaStore.Audio.AudioColumns.DISPLAY_NAME)));
+	long fileSize = cursor.getLong(cursor.getColumnIndex(MediaStore.Audio.Media.SIZE));
+
+	LocalMusic music = new LocalMusic();
+	music.setId(id);
+	music.setType(LocalMusic.Type.LOCAL);
+	music.setTitle(title);
+	music.setArtist(artist);
+	music.setAlbum(album);
+	music.setAlbumId(albumId);
+	music.setDuration(duration);
+	music.setPath(path);
+	music.setFileName(fileName);
+	music.setFileSize(fileSize);
+	musicList.add(music);
+}
+```
+
+#### 1.4 关于实体类介绍
+- 关于实体类,可以直接参考我的demo案例,结合案例看博客会更加有效果
+- 我的案例地址:https://github.com/yangchong211/YCAudioPlayer
+
+
+#### 1.5 注意添加权限,还需要动态申请读取本地文件权限
+```
+<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
+<uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS"/>
+```
+
+
+### 2.如何展示缩略图,歌手,专辑等信息
+#### 2.1 获取缩略图
+```
+/**
+ * 获取小图标
+ * @param music             music
+ * @return                  bitmap对象
+ */
+public Bitmap loadThumbnail(LocalMusic music) {
+	return loadCover(music, Type.THUMBNAIL);
+}
+
+/**
+ * 获取蒙层透明背景bitmap
+ * @param music             music
+ * @return                  bitmap对象
+ */
+public Bitmap loadBlur(LocalMusic music) {
+	return loadCover(music, Type.BLUR);
+}
+
+/**
+ * 获取蒙层透明背景bitmap
+ * @param music             music
+ * @return                  bitmap对象
+ */
+public Bitmap loadRound(LocalMusic music) {
+	return loadCover(music, Type.ROUND);
+}
+
+
+private Bitmap loadCover(LocalMusic music, Type type) {
+	Bitmap bitmap;
+	String key = getKey(music, type);
+	if (TextUtils.isEmpty(key)) {
+		bitmap = mCoverCache.get(KEY_NULL.concat(type.value));
+		if (bitmap != null) {
+			return bitmap;
+		}
+		bitmap = getDefaultCover(type);
+		mCoverCache.put(KEY_NULL.concat(type.value), bitmap);
+		return bitmap;
+	}
+	bitmap = mCoverCache.get(key);
+	if (bitmap != null) {
+		return bitmap;
+	}
+	bitmap = loadCoverByType(music, type);
+	if (bitmap != null) {
+		mCoverCache.put(key, bitmap);
+		return bitmap;
+	}
+	return loadCover(null, type);
+}
+
+
+private String getKey(LocalMusic music, Type type) {
+	if (music == null) {
+		return null;
+	}
+	if (music.getType() == LocalMusic.Type.LOCAL && music.getAlbumId() > 0) {
+		return String.valueOf(music.getAlbumId()).concat(type.value);
+	} else if (music.getType() == LocalMusic.Type.ONLINE && !TextUtils.isEmpty(music.getCoverPath())) {
+		return music.getCoverPath().concat(type.value);
+	} else {
+		return null;
+	}
+}
+
+
+/**
+ * 获取默认的bitmap视图
+ * @param type          类型
+ * @return              bitmap对象
+ */
+private Bitmap getDefaultCover(Type type) {
+	switch (type) {
+		case BLUR:
+			return BitmapFactory.decodeResource(Utils.getContext().getResources(), R.drawable.default_cover);
+		case ROUND:
+			Bitmap bitmap = BitmapFactory.decodeResource(Utils.getContext().getResources(), R.drawable.default_cover);
+			bitmap = ImageUtils.resizeImage(bitmap, ScreenUtils.getScreenWidth() / 2, ScreenUtils.getScreenWidth() / 2);
+			return bitmap;
+		default:
+			return BitmapFactory.decodeResource(Utils.getContext().getResources(), R.drawable.default_cover);
+	}
+}
+
+
+private Bitmap loadCoverByType(LocalMusic music, Type type) {
+	Bitmap bitmap;
+	if (music.getType() == LocalMusic.Type.LOCAL) {
+		bitmap = loadCoverFromMediaStore(music.getAlbumId());
+	} else {
+		bitmap = loadCoverFromFile(music.getCoverPath());
+	}
+	switch (type) {
+		case BLUR:
+			return ImageUtils.blur(bitmap);
+		case ROUND:
+			bitmap = ImageUtils.resizeImage(bitmap, ScreenUtils.getScreenWidth() / 2, ScreenUtils.getScreenWidth() / 2);
+			return ImageUtils.createCircleImage(bitmap);
+		default:
+			return bitmap;
+	}
+}
+
+
+/**
+ * 从媒体库加载封面<br>
+ * 本地音乐
+ */
+private Bitmap loadCoverFromMediaStore(long albumId) {
+	ContentResolver resolver = Utils.getContext().getContentResolver();
+	Uri uri = FileMusicUtils.getMediaStoreAlbumCoverUri(albumId);
+	InputStream is;
+	try {
+		is = resolver.openInputStream(uri);
+	} catch (FileNotFoundException ignored) {
+		return null;
+	}
+	BitmapFactory.Options options = new BitmapFactory.Options();
+	options.inPreferredConfig = Bitmap.Config.RGB_565;
+	return BitmapFactory.decodeStream(is, null, options);
+}
+
+/**
+ * 从下载的图片加载封面<br>
+ * 网络音乐
+ */
+private Bitmap loadCoverFromFile(String path) {
+	BitmapFactory.Options options = new BitmapFactory.Options();
+	options.inPreferredConfig = Bitmap.Config.RGB_565;
+	return BitmapFactory.decodeFile(path, options);
+}
+```
+
+
+### 3.关于其他介绍
+#### 3.3 个人博客
+- **github:** [https://github.com/yangchong211](https://github.com/yangchong211)
+- **知乎:** [https://www.zhihu.com/people/yang-chong-69-24/pins/posts](https://www.zhihu.com/people/yang-chong-69-24/pins/posts)
+- **简书:** [http://www.jianshu.com/u/b7b2c6ed9284](http://www.jianshu.com/u/b7b2c6ed9284)
+- **csdn:** [http://my.csdn.net/m0_37700275](http://my.csdn.net/m0_37700275)
+- **喜马拉雅听书:** [http://www.ximalaya.com/zhubo/71989305/](http://www.ximalaya.com/zhubo/71989305/)
+- 泡在网上的日子:[http://www.jcodecraeer.com/member/content_list.php?channelid=1](http://www.jcodecraeer.com/member/content_list.php?channelid=1)
+- 邮箱:yangchong211@163.com
+- 阿里云博客:[https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV](https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV)
+
+

+ 108 - 0
read/43.音视频编解码操作.md

@@ -0,0 +1,108 @@
+#### **目录介绍**
+- **1.做Android视频编辑的可行性开源方案**
+- 1.1 第三方流行框架
+- 1.2 Android自带框架
+- 1.3 这些方案功能分析
+- 1.4 这些方案学习门槛分析
+- **2.对音频进行编码和解码**
+- 2.1 MediaCodec基本介绍
+- 2.2 MediaCodec核心原理
+- **3.如何通俗理解编码和解码**
+- 3.1 什么是编码
+- 3.2 什么是解码
+- **4.对音频如何混音添加背景音乐**
+- 4.1 待更新实现
+
+
+
+### 1.做Android视频编辑的可行性开源方案
+#### 1.1 第三方流行框架
+- 大家熟知的ffmpeg,将ffmpeg移植到anroid平台,编译成so文件,由jni 调用,可以实现音视频的分离、裁剪、拼合、加字幕、滤镜等功能。
+
+
+#### 1.2 Android自带框架
+- android 自带的MediaCodec 框架,MediaCodec框架底层调用的是StageFright库,StageFright库是默认封装在android系统里面的
+- 官方的 MediaCodec API 虽然支持硬件编解码加速,但是问题和局限还是很多的,一方面是只能在 Android 4.1 以上机型上才能使用,另一方面,由于 Android 手机种类繁多,厂商对底层源码的修改各不相同,导致 MediaCodec API 在实际使用中,会遇到很多坑,有很多兼容性的问题,因此,可以考虑采用第三方的编解码库。
+
+
+#### 1.3 这些方案功能分析
+- **1.3.1 ffmpeg功能分析**
+- ffmpeg 无疑排第一位,他集合了视频编解码、视频滤镜、流媒体推流、音频各种特效等等,基本上你能想到的功能都在里面。
+- 在网上看到说,国内像暴风,某某影音,某某视频等等都用到了该开源框架,
+
+- **1.3.1 MediaCodec功能分析**
+- MediaCodec涵盖了音视频解复用、音频解码、视频解码、音频编码、视频编码、音视频合并的整个流程。跟ffmpeg相比,MediaCodec 更接近底层硬件。这个方案如果想要实现视频的滤镜、字幕、拼接等功能的话,需要自己配合OpenGL ES 来实现,另外,音视频拼接的话,要考虑到不同音频采样率的重采样问题,音频重采用问题,需要懂得傅立叶变换相关的离散信号变换方法,如果要实现音频特效,如变声、均衡器的话,也需要懂得上述信号变换方法。
+
+
+#### 1.4 这些方案学习门槛分析
+- 如果只是做视频转码、加文字、图片特效等,ffmpeg和MediaCodec 旗鼓相当。如果是要拼接视频、做音频的变声、均衡器特效的话,MediaCodec是难度最高的,因为这一切需要你从底层原理做起。
+- 运行效率:MediaCodec硬解硬编最快,ffmpeg硬解硬编方案稍慢。
+- 稳定性: MediaCodec和ffmpeg 的硬解硬编方案旗鼓相当。
+- 打包占用空间:国内最得最好的ffmpeg硬解硬编方案,其so文件在10.几M,MediaCodec由于是纯java 代码,占用空间很容易做到几百K甚至几十K。
+
+
+### 2.对音频进行编码
+#### 2.1 MediaCodec基本介绍
+- 提供了一套访问 Android 底层多媒体模块的接口,主要是音视频的编解码接口
+- Android 应用层统一由 MediaCodec API 来提供各种音视频编解码功能,由参数配置来决定采用何种编解码算法、是否采用硬件编解码加速等等
+
+#### 2.2 MediaCodec核心原理
+- **2.2.1 MediaCodec 使用的基本流程**
+```
+- createEncoderByType/createDecoderByType
+- configure
+- start
+- while(1) {
+    - dequeueInputBuffer
+    - queueInputBuffer
+    - dequeueOutputBuffer
+    - releaseOutputBuffer
+}
+- stop
+- release
+```
+
+- **2.2.2 Buffer队列示意图**
+- ![image](http://s3.51cto.com/wyfs02/M00/7E/7B/wKioL1cCSojSz4HdAAD0xxQxvwg230.png)
+
+- **2.2.3 缓冲处理队列的逻辑**
+- MediaCodec 架构上采用了2个缓冲区队列,异步处理数据,下面描述的 Client 和 MediaCodec 模块是并行工作的(注:这里的 Client 就是指 “开发者,API 的使用者”):
+    * (1)Client 从 input 缓冲区队列申请 empty buffer [dequeueInputBuffer]
+    * (2)Client 把需要编解码的数据拷贝到 empty buffer,然后放入 input 缓冲区队列 [queueInputBuffer] 
+    * (3)MediaCodec 模块从 input 缓冲区队列取一帧数据进行编解码处理
+    * (4)编解码处理结束后,MediaCodec 将原始数据 buffer 置为 empty 后放回 input 缓冲区队列,将编解码后的数据放入到 output 缓冲区队列
+    * (5)Client 从 output 缓冲区队列申请编解码后的 buffer [dequeueOutputBuffer]
+    * (6)Client 对编解码后的 buffer 进行渲染/播放
+    * (7)渲染/播放完成后,Client 再将该 buffer 放回 output 缓冲区队列 [releaseOutputBuffer]
+- MediaCodec 在架构上,其实是采用了一种基于“环形缓冲区”的“生产者-消费者”模式,它设计了 2 个基于 idx 序号的“环形缓冲区” ,注意,是 2 个,一个在 input 端, 一个在 output 端。
+
+
+
+
+### 5.其他问题说明
+#### 5.1 版本更新情况
+- v1.0.0 2017年12月8日
+- v1.0.1 2018年2月2日
+
+
+#### 5.2 参考链接
+- Android硬编码——音频编码、视频编码及音视频混合:http://blog.csdn.net/junzia/article/details/54018671
+- android MediaCodec 音频编解码的实现——转码:https://www.cnblogs.com/Sharley/p/5964490.html
+- Android音频开发(5):音频数据的编解码:http://blog.51cto.com/ticktick/1760191
+- Android 音视频编辑经验总结及开源工程分享:https://www.cnblogs.com/jerrychen33/p/8148993.html
+
+
+#### 5.2 个人博客
+- **github:** [https://github.com/yangchong211](https://github.com/yangchong211)
+- **知乎:** [https://www.zhihu.com/people/yang-chong-69-24/pins/posts](https://www.zhihu.com/people/yang-chong-69-24/pins/posts)
+- **简书:** [http://www.jianshu.com/u/b7b2c6ed9284](http://www.jianshu.com/u/b7b2c6ed9284)
+- **csdn:** [http://my.csdn.net/m0_37700275](http://my.csdn.net/m0_37700275)
+- **喜马拉雅听书:** [http://www.ximalaya.com/zhubo/71989305/](http://www.ximalaya.com/zhubo/71989305/)
+- 泡在网上的日子:[http://www.jcodecraeer.com/member/content_list.php?channelid=1](http://www.jcodecraeer.com/member/content_list.php?channelid=1)
+- 邮箱:yangchong211@163.com
+- 阿里云博客:[https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV](https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV)
+
+
+
+
+

+ 243 - 0
read/43.音频基础知识点.md

@@ -0,0 +1,243 @@
+#### **目录介绍**
+- **1.音频开发具体涉及哪些业务**
+- 1.1 音频播放[这个应该最多]
+- 1.2 音频编辑,比如录音,裁剪等
+- 1.3 音频算法处理,这个很难
+- 1.4 音频的编解码和格式转换
+- 1.5 音频压缩,变声,速率开发
+- **2.了解一些基础概念**
+- 2.1 采样率(samplerate)
+- 2.2 声道数(channels)
+- 2.3 音频帧(frame)
+- **3.音频的压缩,编码与其他**
+- 3.1 常见的音频压缩格式
+- 3.2 常见的音频编码方式
+- 3.3 音频开发相关的API
+- 3.4 关于音频采集录制
+- **4.音频播放常用方法介绍**
+- 4.1 MediaPlayer的常用方法
+- 4.2 MediaPlayer的几个监听方式
+- 4.3 MediaPlayer设置播放源方法
+- 4.4 MediaPlayer方法周期图
+- 4.5 MediaPlayer生命周期分析
+- **5.其他问题说明**
+- 5.1 版本更新情况
+- 5.2 参考链接
+- 5.2 个人博客
+
+
+### 1.音频开发具体涉及哪些业务
+#### 1.1 音频播放[这个应该最多]
+- 可以直接看我的项目代码,基础播放功能有:播放,暂停,下一首,上一首;可以设置多种播放类型:顺序播放,随机播放,单曲循环
+
+#### 1.2 音频编辑,比如录音,裁剪等
+- 比如音频录音功能,录完后可以进行裁剪,比如喜马拉雅,懒人听书等等都有这个功能。
+
+#### 1.3 音频算法处理,这个很难
+- 目前音频算法处理的开源库有:speex、ffmpeg,webrtc等等。
+- 可以使用FFmpeg框架,FFmpeg是一套可以用来记录、转换数字音频、视频,并能将其转化为流的开源计算机程序。采用LGPL或GPL许可证。它提供了录制、转换以及流化音视频的完整解决方案。
+- 具体使用看官方网站:https://www.ffmpeg.org/
+
+#### 1.4 音频的编解码和格式转换
+- 直接参考该系列第五篇文章,很详细介绍了
+
+#### 1.5 音频压缩,变声,速率开发
+- 后期研究,有点复杂,也没找到什么好的案例
+
+
+### 2.了解一些基础概念
+#### 2.1 什么是采样率(samplerate)
+- 采样就是把模拟信号数字化的过程,不仅仅是音频需要采样,所有的模拟信号都需要通过采样转换为可以用0101来表示的数字信号,示意图如下所示:
+- 图片采集于网络,只是作为学习使用
+![image](http://s4.51cto.com/wyfs02/M02/7C/F6/wKioL1bdXQviw_TBAAAVYm_x3gk862.gif)
+- 蓝色代表模拟音频信号,红色的点代表采样得到的量化数值。
+- 采样频率越高,红色的间隔就越密集,记录这一段音频信号所用的数据量就越大,同时音频质量也就越高。
+- 根据奈奎斯特理论,采样频率只要不低于音频信号最高频率的两倍,就可以无损失地还原原始的声音。
+- 通常人耳能听到频率范围大约在20Hz~20kHz之间的声音,为了保证声音不失真,采样频率应在40kHz以上。常用的音频采样频率有:8kHz、11.025kHz、22.05kHz、16kHz、37.8kHz、44.1kHz、48kHz、96kHz、192kHz等。
+
+#### 2.2 声道数(channels)
+- 音频的采集和播放是可以叠加的,因此,可以同时从多个音频源采集声音,并分别输出到不同的扬声器,故声道数一般表示声音录制时的音源数量或回放时相应的扬声器数量。
+- 常见的有单声道(Mono)和双声道(Stereo)
+
+
+#### 2.3 音频帧(frame)
+- 音频跟视频很不一样,视频每一帧就是一张图像,而从上面的正玄波可以看出,音频数据是流式的,本身没有明确的一帧帧的概念,在实际的应用中,为了音频算法处理/传输的方便,一般约定俗成取2.5ms~60ms为单位的数据量为一帧音频。
+- 这个时间被称之为“采样时间”,其长度没有特别的标准,它是根据编解码器和具体应用的需求来决定的,我们可以计算一下一帧音频帧的大小:
+- 假设某通道的音频信号是采样率为8kHz,位宽为16bit,20ms一帧,双通道,则一帧音频数据的大小为:
+- int size = 8000 x 16bit x 0.02s  x 2 = 5120 bit = 640 byte
+
+
+### 3.音频的压缩与编码
+#### 3.1 常见的音频压缩格式
+- 首先简单介绍一下音频数据压缩的最基本的原理:因为有冗余信息,所以可以压缩。
+- 频谱掩蔽效应: 人耳所能察觉的声音信号的频率范围为20Hz~20KHz,在这个频率范围以外的音频信号属于冗余信号。
+- 时域掩蔽效应: 当强音信号和弱音信号同时出现时,弱信号会听不到,因此,弱音信号也属于冗余信号。
+- 简单列出常见的音频压缩格式:MP3,AAC,OGG,WMA,Opus,FLAC,APE,m4a,AMR,等等
+
+
+#### 3.2 常见的音频编码方式
+- 模拟的音频信号转换为数字信号需要经过采样和量化,量化的过程被称之为编码,根据不同的量化策略,产生了许多不同的编码方式,常见的编码方式有:PCM 和 ADPCM,这些数据代表着无损的原始数字音频信号,添加一些文件头信息,就可以存储为WAV文件了,它是一种由微软和IBM联合开发的用于音频数字存储的标准,可以很容易地被解析和播放。
+- 音频开发过程中,会经常涉及到WAV文件的读写,以验证采集、传输、接收的音频数据的正确性。
+
+
+#### 3.3 音频开发相关的API
+- 音频采集:  MediaRecoder,AudioRecord
+- 音频播放:  SoundPool,MediaPlayer,AudioTrack 
+- 音频编解码: MediaCodec
+- NDK API:     OpenSL ES
+- 目前此开源项目采用MediaPlayer播放音频,采集使用AudioRecord
+
+
+#### 3.4 关于音频采集录制
+- 这块没作为重点,如果想深入了解,可以参考大神博客:https://yq.aliyun.com/articles/8637
+- 有点复杂……
+
+
+
+### 4.音频播放常用方法介绍
+#### 4.1 MediaPlayer的常用方法
+- 当然这里仅仅只是列举了一些常用的方法
+- 具体可以参考我的代码:https://github.com/yangchong211/YCAudioPlayer
+- 注意同步加载和异步加载的区别:同步就是指一个进程在执行某个请求的时候,若该请求需要一段时间才能返回信息,那么这个进程将会一直等待下去,直到收到返回信息才继续执行下去;异步是指进程不需 要一直等下去,而是继续执行下面的操作,不管其他进程的状态。当有消息返回时系统 会通知进程进行处理,这样可以提高执行的效率。
+
+```
+MediaPlayer mp = new MediaPlayer();//新建一个的实例
+1.setDataSource() //设置 资源 
+2.start() throws IllegalStateException //开 始播放,如果没有预编译错过会报错 
+3.stop() //停 止播放 
+4.pause() //暂 停播放 
+5.prepare()//同步加载 
+6.prepareAsync()// 异步加载,完成后调用监听 7.isPlaying(); //本 地方法,判断播放状态 
+8.seekTo(int msec) //本地方法,跳转到时间点 9.int getDuration(); //本 地方法,获取音乐总长度 
+10.release() //释 放资源 
+11.reset() //重 置MediaPlayer 
+12.setLooping(boolean looping) //设 置循环 
+13.public native boolean isLooping(); //判 断循环状态 
+14.setVolume(float leftVolume, float rightVolume) 
+/ /设置音量,左声道和右声道 
+15.setVolume(float volume) //设 置音量
+16.getDuration();//获得载入的音频的播放时长
+```
+
+#### 4.2 MediaPlayer的几个监听方式
+- **这里只是展示部分内容,详细内容可以直接去GitHub看我的音频和视频开源案例:https://github.com/yangchong211**
+- **4.2.1 异步加载完成时回调监听**
+
+```
+public void setOnPreparedListener(OnPreparedListener listener) 
+异步监听,一般在异步预加载之前就要设置好。
+```
+
+- **4.2.2 播放完毕后回调监听**
+
+```
+public void setOnCompletionListener(OnCompletionListener listener) 
+一般用于设置播放完毕后,播放下一首还是循环播放
+```
+
+- **4.2.3 跳转完成时的监听**
+
+```
+public void setOnSeekCompleteListener(OnSeekCompleteListener listener) 
+一般用于监听进度突然改变的值的变化
+```
+
+- **4.2.4 更多信息可以直接参考我的代码案例**
+- https://github.com/yangchong211/YCAudioPlayer
+- https://github.com/yangchong211/YCVideoPlayer
+
+
+
+
+#### 4.3 MediaPlayer设置播放源方法
+```
+setDataSource(String path)//指定装载path路径所代表的文件。
+setDataSource(Context context, Uri uri, Map<String, String headers)//指定装载uri所代表的文件。
+setDataSource(Context context, Uri uri)//指定装载uri所代表的文件。
+setDataSource(FileDescriptor fd, long offset, long length)//指定装载fd所代表的文件中从offset开始长度为length的文件内容。
+setDataSource(FileDescriptor fd)//指定装载fd所代表的文件。
+```
+
+
+#### 4.4 MediaPlayer方法周期图[摘自网络]
+![image](https://upload-images.jianshu.io/upload_images/2556431-e04989851ff4c5f7.gif?imageMogr2/auto-orient/)
+
+
+#### 4.5 MediaPlayer生命周期分析
+```
+MediaPlayer是基于状态的,只有在特定状态才能执行特定的方法。所以认清MediaPlayer生命周期十分重要的。
+
+1、当MediaPlayer通过new方式进行初始化或MediaPlayer调用了reset()方法后,它就处于Idle状态。当调用了release()方法后,它就处于End状态。这两种状态之间是MediaPlayer对象的生命周期。
+1.1、在一个新构建的MediaPlayer对象和一个调用了reset()方法的MediaPlayer对象之间有一个微小的但是十分重要的差别。
+在处于Idle状态时,调用getCurrentPosition(), getDuration(), getVideoHeight(), getVideoWidth(), setAudioStreamType(int), setLooping(boolean), setVolume(float, float), pause(), start(), stop(), seekTo(int), prepare() 或者 prepareAsync() 方法都是编程错误。
+当一个MediaPlayer对象刚被构建的时候,内部的播放引擎和对象的状态都没有改变,在这个时候调用以上的那些方法,框架将无法回调客户端程序注册的OnErrorListener.onError()方法;但若这个MediaPlayer对象调用了reset()方法之后,再调用以上的那些方法,内部的播放引擎就会回调客户端程序注册的OnErrorListener.onError()方法了,并将错误的状态传入。
+1.2、一旦一个MediaPlayer对象不再被使用,应立即调用release()方法来释放在内部的播放引擎中与这个MediaPlayer对象关联的资源。资源可能包括如硬件加速组件的单态组件,若没有调用release()方法可能会导致之后的MediaPlayer对象实例无法使用这种单态硬件资源,从而退回到软件实现或运行失败。一旦MediaPlayer对象进入了End状态,它不能再被使用,也没有办法再迁移到其它状态。
+1.3、使用new操作符创建的MediaPlayer对象处于Idle状态,而那些通过重载的create()便利方法创建的MediaPlayer对象却不是处于Idle状态。事实上,如果成功调用了重载的create()方法,那么这些对象已经是Prepare状态了。
+
+2、在 一般情况下,由于种种原因一些播放控制操作可能会失败,如不支持的音频/视频格式,缺少隔行扫描的音频/视频,分辨率太高,流超时等等。因此,错误报告和恢复在这种情况下是非常重要的。有时,由于编程错误,在处于无效状态的情况下调用了一个播放控制操作可能发生。在所有这些错误条件下,内部的播放引擎会调用一个由客户端程序员提供的OnErrorListener.onError()方法。客户端程序员可以通过调用 MediaPlayer.setOnErrorListener(android.media.MediaPlayer.OnErrorListener)方法来注册OnErrorListener。
+2.1、一旦发生错误,MediaPlayer对象会进入到Error状态。
+2.2、为了重用一个处于Error状态的MediaPlayer对象,可以调用reset()方法来把这个对象恢复成Idle状态。
+2.3、注册一个OnErrorListener来获知内部播放引擎发生的错误是好的编程习惯。
+2.4、在不合法的状态下调用一些方法,如prepare(),prepareAsync()和setDataSource()方法会抛出IllegalStateException异常。
+
+3、调 用setDataSource(FileDescriptor)方法,或setDataSource(String)方法,或 setDataSource(Context,Uri)方法,或setDataSource(FileDescriptor,long,long)方法会使处于Idle状态的对象迁移到Initialized状态。
+3.1、若当此MediaPlayer处于其它的状态下,调用setDataSource()方法,会抛出IllegalStateException异常。
+3.2、好的编程习惯是不要疏忽了调用setDataSource()方法的时候可能会抛出的IllegalArgumentException异常和IOException异常。
+
+4、在开始播放之前,MediaPlayer对象必须要进入Prepared状态。
+4.1、有两种方法(同步和异步)可以使MediaPlayer对象进入Prepared状态:要么调用prepare()方法(同步),此方法返回就表示该MediaPlayer对象已经进入了Prepared状态;要么调用prepareAsync()方法(异步),此方法会使此MediaPlayer对象进入Preparing状态并返回,而内部的播放引擎会继续未完成的准备工作。当同步版本返回时或异步版本的准备工作完全完成时就会调用客户端程序员提供的OnPreparedListener.onPrepared()监听方法。可以调用MediaPlayer.setOnPreparedListener(android.media.MediaPlayer.OnPreparedListener)方法来注册OnPreparedListener。
+4.2、Preparing是一个中间状态,在此状态下调用任何具备影响的方法的结果都是未知的!
+4.3、在不合适的状态下调用prepare()和prepareAsync()方法会抛出IllegalStateException异常。当MediaPlayer对象处于Prepared状态的时候,可以调整音频/视频的属性,如音量,播放时是否一直亮屏,循环播放等。
+
+5、要开始播放,必须调用start()方法。当此方法成功返回时,MediaPlayer的对象处于Started状态。isPlaying()方法可以被调用来测试某个MediaPlayer对象是否在Started状态。
+5.1、当处于Started状态时,内部播放引擎会调用客户端程序员提供的OnBufferingUpdateListener.onBufferingUpdate()回调方法,此回调方法允许应用程序追踪流播放的缓冲的状态。
+5.2、对一个已经处于Started 状态的MediaPlayer对象调用start()方法没有影响。
+6、播放可以被暂停,停止,以及调整当前播放位置。当调用pause()方法并返回时,会使MediaPlayer对象进入Paused状态。注意 Started与Paused状态的相互转换在内部的播放引擎中是异步的。所以可能需要一点时间在isPlaying()方法中更新状态,若在播放流内 容,这段时间可能会有几秒钟。
+6.1、调用start()方法会让一个处于Paused状态的MediaPlayer对象从之前暂停的地方恢复播放。当调用start()方法返回的时候,MediaPlayer对象的状态会又变成Started状态。
+6.2、对一个已经处于Paused状态的MediaPlayer对象pause()方法没有影响。
+
+7、调用stop()方法会停止播放,并且还会让一个处于Started,Paused,Prepared或PlaybackCompleted状态的MediaPlayer进入Stopped状态。
+7.1、对一个已经处于Stopped状态的MediaPlayer对象stop()方法没有影响。
+
+8、调用seekTo()方法可以调整播放的位置。
+8.1、seekTo(int)方法是异步执行的,所以它可以马上返回,但是实际的定位播放操作可能需要一段时间才能完成,尤其是播放流形式的音频/视频。当实际的定位播放操作完成之后,内部的播放引擎会调用客户端程序员提供的OnSeekComplete.onSeekComplete()回调方法。可以通过setOnSeekCompleteListener(OnSeekCompleteListener)方法注册。
+8.2、注意,seekTo(int)方法也可以在其它状态下调用,比如Prepared,Paused和PlaybackCompleted状态。此外,目前的播放位置,实际可以调用getCurrentPosition()方法得到,它可以帮助如音乐播放器的应用程序不断更新播放进度
+
+9、当播放到流的末尾,播放就完成了。
+9.1、如果调用了setLooping(boolean)方法开启了循环模式,那么这个MediaPlayer对象会重新进入Started状态。
+9.2、若没有开启循环模式,那么内部的播放引擎会调用客户端程序员提供的OnCompletion.onCompletion()回调方法。可以通过调用MediaPlayer.setOnCompletionListener(OnCompletionListener)方法来设置。内部的播放引擎一旦调用了OnCompletion.onCompletion()回调方法,说明这个MediaPlayer对象进入了PlaybackCompleted状态。
+9.3、当处于PlaybackCompleted状态的时候,可以再调用start()方法来让这个MediaPlayer对象再进入Started状态。
+
+```
+
+
+### 5.其他问题说明
+#### 5.1 版本更新情况
+- v1.0.0 2017年11月14日
+- v1.0.2 2018年1月28日
+
+
+#### 5.2 参考链接
+- Android音频知识介绍-从AndroidRecord看起:https://www.jianshu.com/p/8da3cf058c0f
+- Android音频使用总结:http://blog.csdn.net/wenzhi20102321/article/details/53018738
+- 大神之作,Android MP3录音实现:http://www.cnblogs.com/ct2011/p/4080193.html
+- Android音频开发基础:http://blog.51cto.com/ticktick/1748506?spm=a2c4e.11153940.blogcont8637.16.305f9135xi03I2
+- Android音频焦点机制深入理解:http://blog.csdn.net/wusuobupo/article/details/53034506
+- Android音频框架笔记 :https://www.jianshu.com/p/9481b1482367
+
+
+#### 5.2 个人博客
+- **github:** [https://github.com/yangchong211](https://github.com/yangchong211)
+- **知乎:** [https://www.zhihu.com/people/yang-chong-69-24/pins/posts](https://www.zhihu.com/people/yang-chong-69-24/pins/posts)
+- **简书:** [http://www.jianshu.com/u/b7b2c6ed9284](http://www.jianshu.com/u/b7b2c6ed9284)
+- **csdn:** [http://my.csdn.net/m0_37700275](http://my.csdn.net/m0_37700275)
+- **喜马拉雅听书:** [http://www.ximalaya.com/zhubo/71989305/](http://www.ximalaya.com/zhubo/71989305/)
+- 泡在网上的日子:[http://www.jcodecraeer.com/member/content_list.php?channelid=1](http://www.jcodecraeer.com/member/content_list.php?channelid=1)
+- 邮箱:yangchong211@163.com
+- 阿里云博客:[https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV](https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV)
+
+
+
+
+
+

+ 524 - 0
read/45.音视频加密和解密.md

@@ -0,0 +1,524 @@
+#### **目录介绍**
+- **1.音视频为何需要加密与解密**
+- 1.1 为什么要加密解密
+- 1.2 业务需求分析【先看需求】
+- 1.3 文件加解密的流程及原理【重点】
+- 1.4 常见的加密有哪些
+- **2.利用java包中的加密包进行加密**
+- 2.1 具体使用方法
+- 2.2 有什么优劣势
+- **3.加密音视频前N个字节**
+- 3.1 如何缩短加密时间
+- 3.2 代码逻辑展示
+- 3.3 存在的问题分析
+- **4.自定义实现音视频加密解密**
+- 4.1 思路其实与2目录内容相似
+- 4.2 解决那些问题
+- 4.3 如何监听加密进度,自定义监听事件
+- **5.其他问题说明**
+- 5.1 版本更新情况
+- 5.2 参考链接
+- 5.2 个人博客
+
+
+### 1.音视频为何需要加密与解密
+#### 1.1 为什么要加密解密
+- 项目开发中,之前一直是直接播放网络视频,后来要求加上视频缓存的功能,但是这些视频又都是要付费才能观看的,这就涉及到视频的版权问题。
+- 为了防止一个用户付费下载后,传播视频,就需要给视频文件加密,在播放时解密,只让视频在我的应用中播放。不过,在网上搜索了一下。找到了以下几种加密方法。
+
+
+#### 1.2 业务需求分析
+- 主要是加解密音频和视频,要求下载完后,对于有权限的视频只能在本APP中播放,而不能在其他APP播放
+- 要求加解密时间要短,一般可以保持在30到60秒之间就可以
+- 因为有的音视频权限开放,有的需要权限,这就带来了新问题。如何判断该视频是否已经加密了呢?
+
+#### 1.3 文件加解密的流程及原理【重点】
+- **1.3.1 加密方法**
+- 存储文件时,从输入流中截取文件的字节数组,对字节数组进行加密,至于加密的方式和算法就可以视需求而定了,然后把加密后的字节数组写入到文件中,最后生成加密后的文件;
+- **1.3.2 解密方法**
+- 同加密方法一样,只不过是对字节数据进行解密,最后生成明文文件;
+加密算法:Android系统本身引入了javax包的Cipher类,这个类里提供了各种各样的通用的加密方式,如AES对称加密等;该程序中有个CipherUtil工具类,里面有一些简单的使用Cipher进行AES加解密的方法;当然最好还是好好学习一下Cipher类的使用;
+- **1.3.3 注意事项:**
+- 之前,我看到有些APP将下载完的文件,直接更改为没有后缀名。那么也叫做加密了,因为别人不知道是什么文件,其实还是可以找到同类软件打开的。这种做法是不好的……
+- 如何判断一个文件是加密后的文件,最简单的方法就是对加密后的文件统一增加一个后缀名,然后在解密之后将这个后缀名去除,还原回原有文件格式;如:密文文件的统一后缀名为“.cipher”,明文文件名为"测试.txt",加密后的密文文件应该为“测试.txt.cipher”;
+- 加密文件时还有一个重要的注意事项,就是加密后的密文和明文的长度是否相同,如果文件时一次读取出所有字节数组进行加密的话不用担心这个问题,但是当对文件分次读取加密或分段加密的话,就不得不考虑这个问题了,最方便的方法就是保证明文和加密后的密文长度相同;如果长度不同,由于是分段加密的,密文是由一段一段子密文拼接成的,解密时会找不到每段子密文,因为不知道每段子密文的长度是多少;
+
+
+#### 1.4 常见的加密有哪些
+- 常用加密算法:DES、3DES、RC4、AES,RSA等;
+- 对称加密:des,3des,aes
+- 非对称加密:rsa
+- 不可逆加密:md5
+- 加密模式:ECB、CBC、CFB、OFB等;
+- 填充模式:NoPadding、PKCS1Padding、PKCS5Padding、PKCS7Padding
+
+
+### 2.利用java包中的加密包进行加密**
+#### 2.1 具体使用方法
+- **2.1.1 主要的加密与解密思路是这样的。**
+- 首先第一步是创建一个加解密的key;然后在初始化加载密码;然后再加密某个文件(实际上就已io流输入输出的过程);解密过程也是一样的。
+- **2.1.2 具体的代码是这样的**
+
+```
+public class FileDESUtils {
+
+
+    /**
+     * 这个方法只是解释使用方法,并无意义
+     */
+    private void utils(){
+        FileDESUtils fileDES = null;
+        try {
+            fileDES = new FileDESUtils("spring.sky");
+            //加密调用这个方法
+            fileDES.doEncryptFile("d:/a.mp4", "d:/b");
+            //解密调用这个方法
+            //fileDES.doDecryptFile("d:/b");
+        } catch (Exception e) {
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 加密解密的key
+     */
+    private Key mKey;
+    /**
+     * 解密的密码
+     */
+    private Cipher mDecryptCipher;
+    /**
+     * 加密的密码
+     */
+    private Cipher mEncryptCipher;
+
+    public FileDESUtils(String key) throws Exception {
+        initKey(key);
+        initCipher();
+    }
+
+    /**
+     * 创建一个加密解密的key
+     *
+     * @param keyRule
+     */
+    public void initKey(String keyRule) {
+        byte[] keyByte = keyRule.getBytes();
+        // 创建一个空的八位数组,默认情况下为0
+        byte[] byteTemp = new byte[8];
+        // 将用户指定的规则转换成八位数组
+        for (int i = 0; i < byteTemp.length && i < keyByte.length; i++) {
+            byteTemp[i] = keyByte[i];
+        }
+        mKey = new SecretKeySpec(byteTemp, "DES");
+    }
+
+    /***
+     * 初始化加载密码
+     *
+     * @throws Exception
+     */
+    private void initCipher() throws Exception {
+        mEncryptCipher = Cipher.getInstance("DES");
+        mEncryptCipher.init(Cipher.ENCRYPT_MODE, mKey);
+        mDecryptCipher = Cipher.getInstance("DES");
+        mDecryptCipher.init(Cipher.DECRYPT_MODE, mKey);
+
+    }
+
+    /**
+     * 加密文件
+     *
+     * @param in
+     * @param savePath 加密后保存的位置
+     */
+    public void doEncryptFile(InputStream in, String savePath) {
+        if (in == null) {
+            System.out.println("inputstream is null");
+            return;
+        }
+        try {
+            CipherInputStream cin = new CipherInputStream(in, mEncryptCipher);
+            OutputStream os = new FileOutputStream(savePath);
+            byte[] bytes = new byte[1024];
+            int len = -1;
+            while ((len = cin.read(bytes)) > 0) {
+                os.write(bytes, 0, len);
+                os.flush();
+            }
+            os.close();
+            cin.close();
+            in.close();
+            System.out.println("加密成功");
+        } catch (Exception e) {
+            System.out.println("加密失败");
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 加密文件
+     *
+     * @param filePath 需要加密的文件路径
+     * @param savePath 加密后保存的位置
+     * @throws FileNotFoundException
+     */
+    public void doEncryptFile(String filePath, String savePath) throws FileNotFoundException {
+        doEncryptFile(new FileInputStream(filePath), savePath);
+    }
+
+
+    /**
+     * 解密文件
+     *
+     * @param in
+     */
+    public void doDecryptFile(InputStream in, String path) {
+        if (in == null) {
+            System.out.println("inputstream is null");
+            return;
+        }
+        try {
+            CipherInputStream cin = new CipherInputStream(in, mDecryptCipher);
+            OutputStream outputStream = new FileOutputStream(path);
+            byte[] bytes = new byte[1024];
+            int length = -1;
+            while ((length = cin.read(bytes)) > 0) {
+                outputStream.write(bytes, 0, length);
+                outputStream.flush();
+            }
+            cin.close();
+            in.close();
+            System.out.println("解密成功");
+        } catch (Exception e) {
+            System.out.println("解密失败");
+            e.printStackTrace();
+        }
+    }
+
+    /**
+     * 解密文件
+     *
+     * @param filePath 文件路径
+     * @throws Exception
+     */
+    public void doDecryptFile(String filePath, String outPath) throws Exception {
+        doDecryptFile(new FileInputStream(filePath), outPath);
+    }
+}
+```
+
+
+#### 2.2 有什么优劣势
+- **2.2.1 这种加密解密方法存在的问题**
+- 第一,加密和解密是加解密所有的字节,对于音视频文件来说,读取所有字节会耗时长。
+- 第二,音视频文件一般都是几百兆,用此方法加密的时间就长的有点离谱。
+
+
+
+### 3.加密音视频前N个字节
+#### 3.1 如何缩短加密时间
+- 将音视频文件的数据流前300个字节中的每个字节与其下标进行异或运算。解密时只需将加密过的文件再进行一次异或运算即可。
+
+
+#### 3.2 代码逻辑展示
+
+```
+public class FileNumberEncrypted {
+    private final int REVERSE_LENGTH = 1000;
+    /**
+     * 加解密
+     * 将视频文件的数据流前1000个字节中的每个字节与其下标进行异或运算。
+     * 解密时只需将加密过的文件再进行一次异或运算即可。
+     * @param strFile 源文件绝对路径
+     * @return
+     */
+    private boolean encrypt(String strFile) {
+        int len = REVERSE_LENGTH;
+        try {
+            File f = new File(strFile);
+            RandomAccessFile raf = new RandomAccessFile(f, "rw");
+            long totalLen = raf.length();
+
+            if (totalLen < REVERSE_LENGTH){
+                len = (int) totalLen;
+            }
+            FileChannel channel = raf.getChannel();
+            MappedByteBuffer buffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, REVERSE_LENGTH);
+            byte tmp;
+            for (int i = 0; i < len; ++i) {
+                byte rawByte = buffer.get(i);
+                tmp = (byte) (rawByte ^ i);
+                buffer.put(i, tmp);
+            }
+            buffer.force();
+            buffer.clear();
+            channel.close();
+            raf.close();
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }
+    }
+}
+```
+
+#### 3.3 存在的问题分析
+- 第一,思路很好,但是有个问题,如果判断该视频是否已经加密呢?
+- 第二,RandomAccessFile不属于InputStream和OutputStream类系的,对于读写需要权限支持
+- 第三,程序创建了一个128Mb的文件,如果一次性读到内存可能导致内存溢出,但这里访问好像只是一瞬间的事,因为真正调入内存的只是其中的一小部分,其余部分则被放在交换文件上。这样你就可以很方便地修改超大型的文件了(最大可以到2 GB)。
+
+
+### 4.自定义实现音视频加密解密
+#### 4.1 思路其实与2目录内容相似
+- 还是Android系统本身引入了javax包的Cipher类,这个类里提供了各种各样的通用的加密方式
+- 思路也几乎类似,但是更加严谨
+
+
+#### 4.2 解决那些问题
+- 判断文件是否可以加密或者解密,就看音视频的后缀名是否是自己设置的加密文件后缀名。这个在生活中很常见,一些培训机构将付费视频后缀名自定义设置,只能用专用软件打开。
+- 添加了加密和解密进度监听,通过自定义监听接口实现……
+- 加密和解密不再是对音视频文件前N个字节加解密,而是加解密时以32K个字节为单位进行加解密计算。
+
+
+#### 4.3 如何监听加密进度,自定义监听事件
+- **4.3.1 首先定义一个接口**
+
+```
+public interface CipherProgressListener{
+	/**
+	 * 用于加解密进度的监听器
+	 * @param current           当前进度值
+	 * @param total             总的
+	 */
+	void onProgress(long current, long total);
+}
+```
+
+- **4.3.2 在加密环节添加监听**
+- 加密和解密,添加监听是一样的,没区别
+
+```
+//此处的解密方法很简单,只是简单的异或计算
+for (int j = 0; j < CIPHER_BUFFER_LENGTH; ++j) {
+	rawByte = buffer.get(j);
+	tmp = (byte) (rawByte ^ j);
+	buffer.put(j, tmp);
+	if(null != listener){
+		listener.onProgress(i * CIPHER_BUFFER_LENGTH + j, totalLength);
+	}
+}
+```
+
+- **4.3.3 具体实现**
+- 可以直接参考我的案例:https://github.com/yangchong211/YCAudioPlayer
+- GitHub地址:https://github.com/yangchong211
+- 代码是这样的
+
+```
+public class FileCipherUtils {
+    /**
+     * 加密后的文件的后缀
+     */
+    private static final String CIPHER_TEXT_SUFFIX = ".yc";
+    /**
+     * 加解密时以32K个字节为单位进行加解密计算
+     */
+    private static final int CIPHER_BUFFER_LENGTH = 32 * 1024;
+    /**
+     * 加密,这里主要是演示加密的原理,没有用什么实际的加密算法
+     * @param filePath  明文文件绝对路径
+     * @return          是否加密成功
+     */
+    public static boolean encrypt(String filePath, CipherProgressListener listener) {
+        if(filePath == null){
+            return false;
+        }
+        FileChannel channel = null;
+        RandomAccessFile raf = null;
+        try {
+            long startTime = System.currentTimeMillis();
+            File f = new File(filePath);
+
+            raf = new RandomAccessFile(f, "rw");
+            long totalLength = raf.length();
+            channel = raf.getChannel();
+            long multiples = totalLength / CIPHER_BUFFER_LENGTH;
+            long remainder = totalLength % CIPHER_BUFFER_LENGTH;
+
+            MappedByteBuffer buffer ;
+            byte tmp;
+            byte rawByte;
+            //先对整除部分加密
+            for(int i = 0; i < multiples; i++){
+                buffer = channel.map(FileChannel.MapMode.READ_WRITE,
+                        i * CIPHER_BUFFER_LENGTH, (i + 1) * CIPHER_BUFFER_LENGTH);
+                //此处的加密方法很简单,只是简单的异或计算
+                for (int j = 0; j < CIPHER_BUFFER_LENGTH; ++j) {
+                    rawByte = buffer.get(j);
+                    tmp = (byte) (rawByte ^ j);
+                    buffer.put(j, tmp);
+                    if(null != listener){
+                        listener.onProgress(i * CIPHER_BUFFER_LENGTH + j, totalLength);
+                    }
+                }
+                buffer.force();
+                buffer.clear();
+            }
+
+            //对余数部分加密
+            buffer = channel.map(FileChannel.MapMode.READ_WRITE,
+                    multiples * CIPHER_BUFFER_LENGTH, multiples * CIPHER_BUFFER_LENGTH + remainder);
+            for (int j = 0; j < remainder; ++j) {
+                rawByte = buffer.get(j);
+                tmp = (byte) (rawByte ^ j);
+                buffer.put(j, tmp);
+                if(null != listener){
+                    listener.onProgress(multiples * CIPHER_BUFFER_LENGTH + j, totalLength);
+                }
+            }
+            buffer.force();
+            buffer.clear();
+            //对加密后的文件重命名,增加.cipher后缀
+            //noinspection ResultOfMethodCallIgnored
+            f.renameTo(new File(f.getPath() + CIPHER_TEXT_SUFFIX));
+            LogUtils.e("加密用时:"+(System.currentTimeMillis() - startTime) /1000 + "s");
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        } finally {
+            try {
+                if(channel!=null){
+                    channel.close();
+                }
+                if(raf!=null){
+                    raf.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+
+
+    /**
+     * 解密,这里主要是演示加密的原理,没有用什么实际的加密算法
+     * @param filePath  密文文件绝对路径,文件需要以.cipher结尾才会认为其实可解密密文
+     * @return          是否解密成功
+     */
+    public static boolean decrypt(String filePath, CipherProgressListener listener) {
+        if(filePath == null){
+            return false;
+        }
+        FileChannel channel = null;
+        RandomAccessFile raf = null;
+        try {
+            long startTime = System.currentTimeMillis();
+            File f = new File(filePath);
+            if(!f.getPath().toLowerCase().endsWith(CIPHER_TEXT_SUFFIX)){
+                //后缀不同,认为是不可解密的密文
+                return false;
+            }
+            raf = new RandomAccessFile(f, "rw");
+            long totalLength = raf.length();
+            channel = raf.getChannel();
+
+            long multiples = totalLength / CIPHER_BUFFER_LENGTH;
+            long remainder = totalLength % CIPHER_BUFFER_LENGTH;
+
+            MappedByteBuffer buffer ;
+            byte tmp;
+            byte rawByte;
+
+            //先对整除部分解密
+            for(int i = 0; i < multiples; i++){
+                buffer = channel.map(FileChannel.MapMode.READ_WRITE,
+                        i * CIPHER_BUFFER_LENGTH, (i + 1) * CIPHER_BUFFER_LENGTH);
+                //此处的解密方法很简单,只是简单的异或计算
+                for (int j = 0; j < CIPHER_BUFFER_LENGTH; ++j) {
+                    rawByte = buffer.get(j);
+                    tmp = (byte) (rawByte ^ j);
+                    buffer.put(j, tmp);
+                    if(null != listener){
+                        listener.onProgress(i * CIPHER_BUFFER_LENGTH + j, totalLength);
+                    }
+                }
+                buffer.force();
+                buffer.clear();
+            }
+
+            //对余数部分解密
+            buffer = channel.map(FileChannel.MapMode.READ_WRITE,
+                    multiples * CIPHER_BUFFER_LENGTH, multiples * CIPHER_BUFFER_LENGTH + remainder);
+            for (int j = 0; j < remainder; ++j) {
+                rawByte = buffer.get(j);
+                tmp = (byte) (rawByte ^ j);
+                buffer.put(j, tmp);
+                if(null != listener){
+                    listener.onProgress(multiples * CIPHER_BUFFER_LENGTH + j, totalLength);
+                }
+            }
+            buffer.force();
+            buffer.clear();
+           //对加密后的文件重命名,增加.cipher后缀
+            //noinspection ResultOfMethodCallIgnored
+            f.renameTo(new File(f.getPath().substring(f.getPath().toLowerCase().indexOf(CIPHER_TEXT_SUFFIX))));
+            LogUtils.e("解密用时:"+(System.currentTimeMillis() - startTime) / 1000 + "s");
+            return true;
+        } catch (Exception e) {
+            e.printStackTrace();
+            return false;
+        }finally {
+            try {
+                if(channel!=null){
+                    channel.close();
+                }
+                if(raf!=null){
+                    raf.close();
+                }
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+    }
+    
+    public interface CipherProgressListener{
+        /**
+         * 用于加解密进度的监听器
+         * @param current           当前进度值
+         * @param total             总的
+         */
+        void onProgress(long current, long total);
+    }
+}
+```
+
+### 5.其他问题说明
+#### 5.1 版本更新情况
+- v1.0.0 2017年11月19日
+- v1.0.1 2018年1月17日
+- v1.0.2 2018年2月7日
+
+
+#### 5.2 参考链接
+- Android 视频文件加密:http://blog.csdn.net/qq_24636637/article/details/50524243
+- Android安全开发之浅谈加密算法的坑:https://zhuanlan.zhihu.com/p/24255780?refer=alijaq
+- android中AES加解密的使用方法:http://www.jb51.net/article/96349.htm
+- android中对文件加密解密的实现:http://www.jb51.net/article/95383.htm
+
+#### 5.2 个人博客
+- **github:** [https://github.com/yangchong211](https://github.com/yangchong211)
+- **知乎:** [https://www.zhihu.com/people/yang-chong-69-24/pins/posts](https://www.zhihu.com/people/yang-chong-69-24/pins/posts)
+- **简书:** [http://www.jianshu.com/u/b7b2c6ed9284](http://www.jianshu.com/u/b7b2c6ed9284)
+- **csdn:** [http://my.csdn.net/m0_37700275](http://my.csdn.net/m0_37700275)
+- **喜马拉雅听书:** [http://www.ximalaya.com/zhubo/71989305/](http://www.ximalaya.com/zhubo/71989305/)
+- 泡在网上的日子:[http://www.jcodecraeer.com/member/content_list.php?channelid=1](http://www.jcodecraeer.com/member/content_list.php?channelid=1)
+- 邮箱:yangchong211@163.com
+- 阿里云博客:[https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV](https://yq.aliyun.com/users/article?spm=5176.100239.headeruserinfo.3.dT4bcV)
+
+
+
+
+

+ 0 - 0
read/30.音视频问题考点.md → read/48.音视频问题考点.md


+ 0 - 0
read/39.参考项目和博客说明.md → read/49.参考项目和博客说明.md


+ 0 - 0
read/40.版本更新说明文档.md → read/50.版本更新说明文档.md