Long Luo's Life Notes

每一天都是奇迹

By Long Luo

由于合作的第三方iQiyi视频的数据源更新速度很慢,通过和iQiyi反馈,于是提供了新的API接口。

通过阅读新API接口说明,在发起HTTP Get请求时,必须同时带2个加密的Header参数,分别是时间戳和MD5加密后的key、时间戳以及客户端参数,否则无法返回正确的请求。

目前在Android客户端使用的是Google开源Volley库,支持各种HTTP请求,图片缓存,JSON解析,性能十分强大。之前使用的接口都是直接发起HTTP Get请求,附带相关参数即可。

通过阅读Volley相关资料,找到了下列方法,可以在发起HTTP请求时,附带Header参数,代码如下所示:

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
 public void makeHTTPrequest(String url) {
MyLog.d(TAG, "makeHTTPrequest, url=" + url);

JsonObjectRequest jsonObjRequest = new JsonObjectRequest(Request.Method.GET, url, null,
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
try {
MyLog.d(TAG, "response=" + response);
parseiQiyiInterfaceResponse(response);
} catch (Exception e) {
e.printStackTrace();
}
}
}, new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
if (error instanceof NetworkError) {
} else if (error instanceof ClientError) {
} else if (error instanceof ServerError) {
} else if (error instanceof AuthFailureError) {
} else if (error instanceof ParseError) {
} else if (error instanceof NoConnectionError) {
} else if (error instanceof TimeoutError) {
}

MyLog.e(TAG, "onErrorResponse, error=" + error);
}
}) {
@Override
public Map<String, String> getHeaders() throws AuthFailureError {
HashMap<String, String> headers = new HashMap<String, String>();
headers.put("t", iQiyiInterface.getEncryptTimestamp());
headers.put("sign", iQiyiInterface.getSign());

// MyLog.d(TAG, "headers=" + headers);
return headers;
}
};

// Set a retry policy in case of SocketTimeout & ConnectionTimeout
// Exceptions. Volley does retry for you if you have specified the
// policy.
jsonObjRequest.setRetryPolicy(new DefaultRetryPolicy(5000,
DefaultRetryPolicy.DEFAULT_MAX_RETRIES, DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
jsonObjRequest.setTag(TAG);
jsonObjRequest.setShouldCache(true);

mQueue.add(jsonObjRequest);
mQueue.start();
}

Header参数使用HashMap存储。

获取到JSON数据之后,剩下的就是解析数据了,在这里就不赘述了。

在完成这个过程中,还遇到了很多小插曲,比如Header的sign值不支持大写字母,结果前后也白费了不少力气。apk烧写到手机之后,还需要使用tcpdump抓取数据包,看是否返回了正确的数据。

最后发现了Chrome的一个Smart Header插件,完美的解决了以上问题,不需要每次抓包验证返回结果了,直接在Chrome浏览器即可,节省了大量时间。

总之,希望这篇文章对大家能有所帮助。谢谢!

Created by Long Luo at 2014-09-07 12:30:03 @Shenzhen, China.

By Long Luo

最近几天学习了下网页开发,通过学习,完成了第一个Web前端开发项目:天气应用

数据源是Yahoo! Weather

使用了jQuery库开发,虽然现在啥都不会,也就到处copy。

Created by Long Luo at 2014-09-07 11:36:20 @Shenzhen, China.

By Long Luo

一、重构前的一些缺点

  1. 频道显示在 VideoListActivity 实现的,代码结构不够清晰;
  2. 频道页面需要根据不同频道目前存在矩阵式显示和列表式显示 2 种方式,矩阵式显示嵌入在 ListView 中实现的,造成加载时绘制矩阵式页面需要更长的时间;
  3. 后续频道页面会参考第三方视频应用,不同频道会有不同的展示方式,而目前功能可扩展性不够好;

二、重构方案

重构方案:

  1. 将频道页面独立出来,新建一个 ChannelListActivity ,作为所有频道页面的 Activity ,负责所有频道界面的绘制,便于后续扩展;
  2. 针对不同频道,具有不同的布局方式,比如对于需要矩阵式显示的频道,使用矩阵式布局;
  3. 矩阵式布局,替换原有的 ListViewGridView 显示,提高加载速度;

三、具体实现

技术实现方案如下所示:

  1. 新建 ChannelListActivity ,在 VideoListActivity 中用进入频道页的接口替换原有的接口,将相关处理在 ChannelListActivity 中实现;
  2. 根据传入的频道 ID 不同,使用 setContentView() 加载不同的布局方式,实现不同频道不同布局显示;
  3. 新建 ChannelGridAdapter ,替换原有的 ChannelListAdapterChannelGridListAdapter 方式,提高加载速度;

四、数据对比

我们通过 TraceView 工具比较前后 2 种方案的区别:

4.1 启动时间:

比较两者启动时间,可以看出重构之后的方案是有大概20ms的提升。

Startup Time

4.2 TraceView对比:

通过 DDMS TraceView 工具,我们再比较下两者性能上的区别:

4.2.1 重构之前:

重构之前的在绘制页面时需要绘制 ListView ,然后在每一个 List 中在绘制 GridView ,在 TraceView 获取到的数据如下:

ChannelListAdapter
ChannelGridListAdapter

从以上可以分析,在 ChannelListAdaptergetView() 就占据了1.0%的 Incl Cpu Time , GridViewgetView() 占据了6.9%的 Incl Cpu Time ,合计占用了7.9%的 CPU Time。

4.2.2 重构之后:

重构之后,我们仅需要在 ChannelGridAdapter 中绘制 View,测量数据如下:

ChannelGridAdapter

从上图可以看出, getView() 占据的 CPU 时间仅为3.2%,效率大大提高。

文章修改历史

  • Created by Long Luo at 2014/7/1 15:45:01
  • Completed by Long Luo at 2014/7/2 14:42:29
  • Modified by Long Luo at 2018年9月27日22点40分 at Hangzhou, China.
  • 修改图片图床 2024.03.03 in Shenzhen.

By Long Luo

一、在线搜索

之前搜索页面的一些缺陷:

  1. 具体实现位于 VideoListActivity 中,一方面会造成 VideoListActivity 代码过于庞大臃肿,另外一方面不便于后续功能扩展,结构不清晰;
  2. 依赖了大量系统控件,不便于后续解耦界面定制
  3. 今后搜索界面会参考第三方视频应用的实现,之前不便于增加搜索记录,或搜索独立出来,用于搜索本地视频,甚至将此搜索移植应用于其他应用中;

1.1 在线搜索实现效果

在线搜索因为是和第三方合作,涉及到很多网络相关的操作,简单来说就是利用 Http 协议向相关接口发起一次网络请求,服务器如果返回了正确的响应,App 会解析服务器返回的内容,并展示出来。

1.1.1 热词界面

热词界面是当搜索文本框文字为空时会弹出热词界面,会展示最近一段时间内搜索频率很高的词语。一方面可以节省大家输入文字,另外一方面你也可以了解当前的一些热点。

当你点击列表中的某个热词时,就会发起一次以此为关键词的搜索。

热词显示

1.1.2 关联词

搜索文本输入框含有文字时,会获取当前输入文字,以此为关键词获取网络的一些联想词,可以点击此联想词发起一次搜索。

关联词显示

1.1.3 搜索结果分类浏览

发起一次搜索之后,如果得到了服务器的正确响应,而且确实有相关视频内容。那么我们会将搜索结果展示在手机页面上。

搜索到的结果可以分不同频道浏览,会根据具体内容进行动态变化,有的可能有十几个频道,有的也就一个频道。频道页面可以滑动浏览,也可以选择在顶部页面选中或者滑动。

分类浏览时,第一个展示的页面是搜索到的全部视频内容,之后的会根据结果动态变化。

如下图所示:

搜索结果

分频道浏览:

搜索结果分类

1.1.4 语音搜索

语音搜索图标只有当搜索框里文字为空才会出现,否则出现搜索图标

点击语音搜索图标将会启动 VoiceSearch 这个 apk ,然后你可以说话,如果被正确识别之后,会发起一次搜索,并将结果展示出来。

语音搜索

1.1.5 语音搜索结果

语音搜索结果
阅读全文 »

翻译 By Long Luo

原文链接:Android Audio: Play a WAV file on an AudioTrack

译者注: 1. 由于这是技术文章,所以有些词句使用原文,表达更准确。 2. 由于水平有效,有些地方可能翻译的不够准确,如有不当之处,敬请批评指正. 3. 针对某些语句,适当补充了上下文及更适合中文阅读,尽量做到信达雅。


如果你已经成功地了解了关于AudioTrack一些话题,那么你可能享受它带来的好处,例如低延迟(在STATIC(静态)模式),能够生成流式音频(在STREAM(流)模式)以及在播放之前,就能够访问和修改原始声音数据。

不过,现在的问题是如何从源获取数据。许多应用需要使用的AudioTrack并不能简单的生成PCM音频(一个例子,比如Ethereal Dialpad或者其他类似的App)。你可能需要从文件源去加载数据,例如WAVMP3文件。

不要期望使用MediaPlayer,去解码WAV文件和MP3音频。虽然MediaPlayer播放这些文件非常好,但是其播放逻辑完全在Native层,同时并没有为我们提供额外选项,允许我们使用其他解码器实现我们的目的。因此,我们必须从手动地从音频文件进行解码出PCM

在这篇文章中,将会讨论WAV格式文件。而在下一课中,我们将会更进一步,讨论如何从MP3文件读取音频。

背景知识: 一些数字音频术语

如果你的App不是专门为数字音频设计,那么在继续我们的讨论之前,你可能需要先了解一些基本的缩略语。别担心,都很简单,我们不需要对此做深入挖掘。

  • PCM(脉冲调制方式) - 实现一个物理音频信号变成数字化最简单方法。基本原理就是信号变成了一个数字阵列,而其中每个数字代表的是声音在特定的时间瞬间的电平也可以说是能量(振幅)。(如果这种解释在科学上可能不会很准确,那我就只能说声抱歉了)。信不信由你,你可以使用这种方法表示任何复杂的声音,而且回放出来也非常精准。在这里,我们将只会谈到线性PCM。在线性PCM中,其中阵列中的每个数字都是原始声音振幅的线性表示。在某些情况下,对数映射能够更好地表示原来的声音幅度比例情况 - 但是我们不会讨论那些情况。

  • Sampling rate(采样率):- 每秒你的数字声音有多少样本(声音幅度用数字表示)。样本越多,你能得到声音质量越好。目前在消费类音频系统目前使用的采样率通常是22050,44100和48000Hz/s。

  • 每个样品分辨率/采样大小/位 - 定义表示振幅数字的大小和格式。例如,如果您使用的是8位整数,你只能表达出256级的幅度,所以原来的物理波形将被简化为256个离散电平,与此同时,你将失去一些声音精度也可以说是质量。如果你使用16位,那么声音质量变得更好。事实上,大部分时间你可能会使用16位音频。其他选项包括24位,32位(这些都是Android现在不支持的),或是使用浮点数。

  • 声道 - 既可以是单声道,也可以是立体声(2个声道),或者更多声道(但是Android不支持)。如果你想要有立体声,你需要有立体声音频,就必须要在每个声道都需要有一个独立的PCM数组,相应的信息量也会翻倍。

上述定义也有助于你理解特定的格式和长度的音频缓冲区的数据量,以便提前预备缓冲区。也就是你需要一个缓冲区,以用于存储5秒长度以44100Hz采样率的立体声16-bit线性PCM数据。数据计算公式如下所示:

5 sec * 44100 samples per sec * 2 bytes per sample * 2 channels = 882,000 bytes

这一数额所需的内存可能会让初学者感到惊讶,因为当你往你的磁盘上存储的音频时,一个MP3文件,一个880KB的文件就可以容纳以相同的采样率和分辨率1分钟时长的音轨。这是为什么呢?因为先进的格式,比如MP3格式。因为我们大脑无法分辨识别出一些音频的内容,所以使用了很多复杂的方式在压缩的过程中去掉了这些内容。然而,大多数低等级的音频API,包括Android的AudioTrack只能接受线性PCM。这就是为什么如果我们不能把整个样品都放在内存中,我们需要将要处理的数据流,循环缓冲区和其他聪明的方式来使用音频API。

希望这样的解释并没有让你产生困惑,现在让我们继续来实际做一些与Android上的数字音频有关的工作吧!

WAV文件格式

我们的目标是用一个InputStream,由其从一个WAV文件加载PCM数据,来提供原始字节数据。然后我们就可以将原始的PCM数据直接推送到使用已经正确的配置好了的AudioTrack.write,通过使用AudioTrack.write()这个API。

WAV文件包含一个文件头和具体数据会。我们需要读取文件头以知道诸如采样速率,分辨率等信息。另外,我们通过文件头,也可以知道此格式是否支持。WAV可以封装成多种格式,我们无法全部支持。也许,只是合理的采样率,分辨率和通道的线性PCM格式。

WAV格式的细节在互联网上都可以找到,你仅仅需要在Google上搜索下。但是,遗憾的是,我并没有搜索到一个很好的Java库来读取WAV文件,而且可以移植到Android下。因此,我自己写了一些简单的代码。

下面这个方法就是如何读取一个WAV文件的头部:

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
private static final String RIFF_HEADER = "RIFF";
private static final String WAVE_HEADER = "WAVE";
private static final String FMT_HEADER = "fmt ";
private static final String DATA_HEADER = "data";

private static final int HEADER_SIZE = 44;

private static final String CHARSET = "ASCII";

/* ... */

public static WavInfo readHeader(InputStream wavStream) throws IOException,
DecoderException {

ByteBuffer buffer = ByteBuffer.allocate(HEADER_SIZE);
buffer.order(ByteOrder.LITTLE_ENDIAN);

wavStream.read(buffer.array(), buffer.arrayOffset(), buffer.capacity());

buffer.rewind();
buffer.position(buffer.position() + 20);
int format = buffer.getShort();
checkFormat(format == 1, "Unsupported encoding: " + format); // 1 means
// Linear
// PCM
int channels = buffer.getShort();
checkFormat(channels == 1 || channels == 2, "Unsupported channels: "
+ channels);
int rate = buffer.getInt();
checkFormat(rate <= 48000 && rate >= 11025, "Unsupported rate: " + rate);
buffer.position(buffer.position() + 6);
int bits = buffer.getShort();
checkFormat(bits == 16, "Unsupported bits: " + bits);
int dataSize = 0;
while (buffer.getInt() != 0x61746164) { // "data" marker
Log.d(TAG, "Skipping non-data chunk");
int size = buffer.getInt();
wavStream.skip(size);

buffer.rewind();
wavStream.read(buffer.array(), buffer.arrayOffset(), 8);
buffer.rewind();
}
dataSize = buffer.getInt();
checkFormat(dataSize > 0, "wrong datasize: " + dataSize);

return new WavInfo(new FormatSpec(rate, channels == 2), dataSize);
}

上面的代码中,缺少的部分应该是显而易见的。正如你所看到的,仅仅支持16位,但在你可以修改代码以支持8位(AudioTrack不支持任何其他分辨率的)。

下面这个方法,则是用来读取文件剩余的部分 - 音频数据

1
2
3
4
5
6
public static byte[] readWavPcm(WavInfo info, InputStream stream)
throws IOException {
byte[] data = new byte[info.getDataSize()];
stream.read(data, 0, data.length);
return data;
}

我们读取的WavInfo结构体,包含采样率,分辨率和声道数已经足够让我们去播放我们读取的音频了。

如果我们不需要将全部音频数据一次性放入内存中,我们可以使用一个InputStream,一点一点地读取。

阅读全文 »
0%