07 Nov 2013
#include <iostream>
#include <stdio.h>
#include <string.h>
using namespace std;
double hexStr2double(const unsigned char* hex, const unsigned length) {
double ret = 0;
char hexstr[length * 2];
memset(hexstr, 0, sizeof(hexstr));
for(unsigned int i = 0; i < length; i++) {
sprintf(hexstr + i * 2, "%02x", hex[i]);
}
sscanf(hexstr, "%llx", (unsigned long long*)&ret);
return ret;
}
int main() {
double ret = 0;
//40 9e 00 00 00 00 00 00
unsigned char buffer[8] = {0x40, 0x9e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
ret = hexStr2double(buffer, 8);
printf("%f\n", ret);
//40 89 00 00 00 00 00 00
ret = 0;
unsigned char buffer2[8] = {0x40, 0x89, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
ret = hexStr2double(buffer2, 8);
printf("%f\n", ret);
}
07 Nov 2013
activity_main.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="vertical" >
<ListView
android:id="@+id/lv"
android:layout_width="fill_parent"
android:layout_height="fill_parent" />
</LinearLayout>
list_view.xml
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="fill_parent"
android:layout_height="fill_parent"
android:orientation="horizontal" >
<ImageView
android:id="@+id/imgView"
android:layout_width="80dp"
android:layout_height="80dp" />
<TextView
android:id="@+id/tv"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>
MainActivity.java
public class MainActivity extends Activity implements OnItemClickListener {
Cursor cursor;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
ListView listView = (ListView) findViewById(R.id.lv);
String[] thumbCols = { MediaStore.Video.Thumbnails.DATA,
MediaStore.Video.Thumbnails.VIDEO_ID };
String[] videoCols = { MediaStore.Video.Media._ID,
MediaStore.Video.Media.DATA, MediaStore.Video.Media.TITLE,
MediaStore.Video.Media.MIME_TYPE };
cursor = managedQuery(MediaStore.Video.Media.EXTERNAL_CONTENT_URI,
videoCols, null, null, null);
ArrayList<VideoInfo> videos = new ArrayList<VideoInfo>();
if (cursor != null && cursor.moveToFirst()) {
do {
VideoInfo vf = new VideoInfo();
int id = cursor.getInt(cursor
.getColumnIndex(MediaStore.Video.Media._ID));
//查询缩略图
Cursor thumbCursor = managedQuery(
MediaStore.Video.Thumbnails.EXTERNAL_CONTENT_URI,
thumbCols, MediaStore.Video.Thumbnails.VIDEO_ID + "="
+ id, null, null);
if (thumbCursor != null && thumbCursor.moveToFirst()) {
vf.thumbPath = thumbCursor.getString(thumbCursor
.getColumnIndex(MediaStore.Video.Thumbnails.DATA));
Log.v("Debug thumPath:", vf.thumbPath);
}
vf.videoPath = cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.DATA));
vf.title = cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.TITLE));
vf.mimeType = cursor.getString(cursor
.getColumnIndex(MediaStore.Video.Media.MIME_TYPE));
videos.add(vf);
} while (cursor.moveToNext());
listView.setAdapter(new VideoGalleryAdapter(this, videos));
listView.setOnItemClickListener(this);
}
}
private class VideoInfo {
String videoPath;
String thumbPath;
String mimeType;
String title;
}
private class VideoGalleryAdapter extends BaseAdapter {
private Context context;
private ArrayList<VideoInfo> videos;
public VideoGalleryAdapter(Context _context,
ArrayList<VideoInfo> _videos) {
context = _context;
videos = _videos;
}
@Override
public int getCount() {
return videos.size();
}
@Override
public Object getItem(int position) {
return videos.get(position);
}
@Override
public long getItemId(int position) {
return position;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
//复用同一个View
if(convert == null) {
LayoutInflater inflater;
// 绑定一个layout xml文件
inflater = (LayoutInflater) context
.getSystemService(Context.LAYOUT_INFLATER_SERVICE);
View row = inflater.inflate(R.layout.list_view, null);
ImageView thumb = (ImageView) row.findViewById(R.id.imgView);
if (videos.get(position).thumbPath != null) {
thumb.setImageURI(Uri.parse(videos.get(position).thumbPath));
}
TextView title = (TextView) row.findViewById(R.id.tv);
title.setText(videos.get(position).title);
return row;
} else {
ImageView thumb = (ImageView) convertView.findViewById(R.id.imgView);
if (videos.get(position).thumbPath != null) {
thumb.setImageURI(Uri.parse(videos.get(position).thumbPath));
}
TextView title = (TextView) convertView.findViewById(R.id.tv);
title.setText(videos.get(position).title);
return convertView;
}
}
@Override
public void onItemClick(AdapterView<?> arg0, View arg1, int arg2, long arg3) {
if (cursor.moveToPosition(arg2)) {
int filePathIdx = cursor
.getColumnIndex(MediaStore.Video.Media.DATA);
int mimeTypeIdx = cursor
.getColumnIndex(MediaStore.Video.Media.MIME_TYPE);
String filePath = cursor.getString(filePathIdx);
String mimeType = cursor.getString(mimeTypeIdx);
Intent intent = new Intent(android.content.Intent.ACTION_VIEW);
File file = new File(filePath);
intent.setDataAndType(Uri.fromFile(file), mimeType);
startActivity(intent);
}
}
}
ref: Pro Android Media: Developing Graphics, Music, Video and Rich Media Apps for Smartphones and Tablets
git clone https://github.com/lnmcc/VideoGallery.git
06 Nov 2013
FLV文件组成
FLV文件由Header和Body两部分组成。
域名 |
类型 |
说明 |
Signature |
UI8 |
Signature byte always ‘F’ (0x46) |
Signature |
UI8 |
Signature byte always ‘L’ (0x4C) |
Signature |
UI8 |
Signature byte always ‘V’ (0x56) |
Version |
UI8 |
File version (for example, 0x01 for FLV version 1) |
TypeFlagsReserved |
UB[5] |
Must be 0 |
TypeFlagsAudio |
UB[1] |
Audio tags are present |
TypeFlagsReserved |
UB[1] |
Must be 0 |
TypeFlagsVideo |
UB[1] |
Video tags are present |
DataOffset |
UI32 |
Offset in bytes from start of file to start of body (that is, size of header) |
FLV文件都是以’F”L”V’3个字符开始。在FLV version 1 中,DataOffset一般是9,也就是说FLV的头占据9个字节。

FLV文件头解析片段:
#define FLV_HEAD_LEN 9
FlvHeader* FlvReader::readHeader() {
unsigned char *header = new unsigned char[FLV_HEAD_LEN];
size_t readBytes = fread(header, sizeof(unsigned char), FLV_HEAD_LEN, m_fp);
if(readBytes == FLV_HEAD_LEN) {
if(header[0] != 'F' || header[1] != 'L' || header[2] != 'V') {
cerr << "Not a FLV file!!!" << endl;
delete header;
return NULL;
}
return new FlvHeader(header, FLV_HEAD_LEN);
} else {
delete header;
return NULL;
}
}
FLV Body
域名 |
类型 |
说明 |
PreviousTagSize0 |
UI32 |
Always 0 |
Tag1 |
FLVTAG |
First tag |
PreviousTagSize1 |
UI32 |
Size of previous tag, including itsheader. For FLV version 1, this valueis 11 plus the DataSize of the previoustag. |
Tag2 |
FLVTAG |
Second tag |
… |
… |
… |

图片来自网络
FLV的Body部分是由许多个TAG组成,每一个TAG也是有Tag Header和Tag Body组成。查看上图注意到,每一个TAG之前都有一个PreviousTagSize,一个unsigned int值,用来指示本TAG之前那个TAG的长度,唯一的例外是紧跟在FLV Header之后的那个PreviousTagSize,它的值一定是0 。

FLV Tag组成
每一个TAG也是由Tag Header和Tag Body组成。
域名 |
类型 |
说明 |
TagType |
UI8 |
Type of this tag. Values are:8: audio9: video18: script dataall others: reserved |
DataSize |
UI24 |
Length of the data in the Data field |
Timestamp |
UI24 |
Time in milliseconds at which thedata in this tag applies. This value isrelative to the first tag in the FLVfile, which always has a timestampof 0 |
TimestampExtended |
UI8 |
Extension of the Timestamp field toform a UI32 value. This fieldrepresents the upper 8 bits, whilethe previous Timestamp fieldrepresents the lower 24 bits of thetime in milliseconds |
StreamID |
UI24 |
Always 0 |
Data |
If TagType = 8AUDIODATAIf TagType = 9VIDEODATAIf TagType = 18SCRIPTDATAOBJECT |
Body of the tag |
一个TagHeader占据11个字节

Tag类型分类
script data
script data: 十六进制值为0x12(18)。这个类型的Tag通常被称为Metadata Tag,存有关于FLV视频和音频的参数信息,如duration、width、height等。通常该类型Tag会紧跟在FLV Header之后,一个FLV只能有一个Metadata Tag。
metadata Tag由两个AMF(Action Message Format)组成:
-
第1个AMF: 第1个字节表示AMF包类型,一般为0x02,表示后面跟的数据是字符串
第2,3个字节为UI16类型值,表示后面字符串的长度,一般为0x000A(即“onMetaData”的长度)
第4-13个字节为字符串数据,一般为“onMetaData”

-
第2个AMF: 第1个字节表示AMF包类型,一般为0x08,表示后面的数据部分是一个数组

第2-5个字节为UI32类型值,表示数组元素的个数,下图所示00000011(HEX) = 17(DEC),表示名称/值数组长度为17

后面即为各数组元素的封装,数组元素为名称/值对。表示方法如下:
第1-2个字节表示元素名称的长度,假设为L,读取后面的L个字节即可得到这个名称字符串。下图所示0008(HEX)=8(DEC),表示这个名称字符串的长度为8 (实际就是“duration”的长度)

“duration”字符串

接下来1byte是值类型,下图的00(HEX)表示Number type,占8byte,这样后面的8byte即为值(这里需要把16进制数转换成double)

AMF的格式:数据类型 + 数据长度 + 数据
数据类型占 1byte
AMF数据类型有:
0 = Number type (DOUBLE : 8byte)
1 = Boolean type (UI8 : 1byte)
2 = String type (2byte)
3 = Object type
4 = MovieClip type
5 = Null type
6 = Undefined type
7 = Reference type (UI16 : 2byte)
8 = ECMA array type
10 = Strict array type
11 = Date type
12 = Long string type (4byte)
上图中的0x12(18)意味着这个Tag是一个script。后面3个字节是Tag Body的大小。
读取一个MetaData的代码片段:
FlvMetaData* FlvReader::readMeta() {
unsigned char tagHeader[TAG_HEAD_LEN];
size_t readBytes;
fseek(m_fp, 9 + 4, SEEK_SET);
//read Tag header
readBytes = fread(tagHeader, sizeof(unsigned char), TAG_HEAD_LEN, m_fp);
if(readBytes == TAG_HEAD_LEN) {
//0x12: meta data
if(tagHeader[0] == 0x12 ) {
unsigned int tagBodySize = 0;
tagBodySize |= tagHeader[1];
tagBodySize = tagBodySize << 8;
tagBodySize |= tagHeader[2];
tagBodySize = tagBodySize << 8;
tagBodySize |= tagHeader[3];
//cerr << "MetaBody size: " << tagBodySize << endl;
unsigned char *tag = new unsigned char[tagBodySize + TAG_HEAD_LEN];
memcpy(tag, tagHeader, TAG_HEAD_LEN);
readBytes = fread(&tag[TAG_HEAD_LEN], sizeof(unsigned char), tagBodySize, m_fp);
if(readBytes == tagBodySize) {
return new FlvMetaData(tag, tagBodySize + TAG_HEAD_LEN);
} else {
delete tag;
return NULL;
}
}
}
return NULL;
}
分析一个MetaData的代码片段:
void FlvMetaData::parseMeta() {
unsigned int arrayLen = 0;
unsigned int offset = TAG_HEAD_LEN + 13;
unsigned int nameLen = 0;
double numValue = 0;
bool boolValue = false;
if(m_meta[offset++] == 0x08) {
arrayLen |= m_meta[offset++];
arrayLen = arrayLen << 8;
arrayLen |= m_meta[offset++];
arrayLen = arrayLen << 8;
arrayLen |= m_meta[offset++];
arrayLen = arrayLen << 8;
arrayLen |= m_meta[offset++];
} else {
//TODO:
cerr << "metadata format error!!!" << endl;
return ;
}
for(unsigned int i = 0; i < arrayLen; i++) {
numValue = 0;
boolValue = false;
nameLen = 0;
nameLen |= m_meta[offset++];
nameLen = nameLen << 8;
nameLen |= m_meta[offset++];
char name[nameLen + 1];
memset(name, 0, sizeof(name));
memcpy(name, &m_meta[offset], nameLen);
name[nameLen + 1] = '\0';
offset += nameLen;
switch(m_meta[offset++]) {
case 0x0: //Number type
numValue = hexStr2double(&m_meta[offset], 8);
offset += 8;
break;
case 0x1: //Boolean type
if(offset++ != 0x00) {
boolValue = true;
}
break;
case 0x2: //String type
nameLen = 0;
nameLen |= m_meta[offset++];
nameLen = nameLen << 8;
nameLen |= m_meta[offset++];
offset += nameLen;
break;
case 0x12: //Long string type
nameLen = 0;
nameLen |= m_meta[offset++];
nameLen = nameLen << 8;
nameLen |= m_meta[offset++];
nameLen = nameLen << 8;
nameLen |= m_meta[offset++];
nameLen = nameLen << 8;
nameLen |= m_meta[offset++];
offset += nameLen;
break;
//FIXME:
default:
break;
}
if(strncmp(name, "duration", 8) == 0) {
m_duration = numValue;
} else if(strncmp(name, "width", 5) == 0) {
m_width = numValue;
} else if(strncmp(name, "height", 6) == 0) {
m_height = numValue;
} else if(strncmp(name, "framerate", 9) == 0) {
m_framerate = numValue;
} else if(strncmp(name, "videodatarate", 13) == 0) {
m_videodatarate = numValue;
} else if(strncmp(name, "audiodatarate", 13) == 0) {
m_audiodatarate = numValue;
} else if(strncmp(name, "videocodecid", 12) == 0) {
m_videocodecid = numValue;
} else if(strncmp(name, "audiosamplerate", 15) == 0) {
m_audiosamplerate = numValue;
} else if(strncmp(name, "audiosamplesize", 15) == 0) {
m_audiosamplesize = numValue;
} else if(strncmp(name, "audiocodecid", 12) == 0) {
m_audiocodecid = numValue;
} else if(strncmp(name, "stereo", 6) == 0) {
m_stereo = boolValue;
}
}
}
git clone https://github.com/lnmcc/FlvParser.git
04 Nov 2013
MediaPlayer状态变换图:

图片来自Google Android Doc
public class MainActivity extends Activity {
Display display;
SurfaceHolder surfaceHolder;
SurfaceView surfaceView;
MediaPlayer mediaPlayer;
// 视频宽高
int width = 0;
int height = 0;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
display = getWindowManager().getDefaultDisplay();
String file = Environment.getExternalStorageDirectory()
.getAbsolutePath() + "/Movies/1.m4v";
surfaceView = (SurfaceView) findViewById(R.id.sv);
surfaceHolder = surfaceView.getHolder();
surfaceHolder.addCallback(new SurfaceHolder.Callback() {
// 当SurfaceView销毁时调用
@Override
public void surfaceDestroyed(SurfaceHolder holder) {
// TODO Auto-generated method stub
}
@Override
public void surfaceCreated(SurfaceHolder holder) {
// MediaPlayer通过SurfaceHolder与SurfaceView联系在一起
mediaPlayer.setDisplay(holder);
}
// 当SurfaceView大小发生变化时调用
@Override
public void surfaceChanged(SurfaceHolder holder, int format,
int width, int height) {
// TODO Auto-generated method stub
}
});
mediaPlayer = new MediaPlayer();
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
// TODO Auto-generated method stub
}
});
mediaPlayer.setOnCompletionListener(new MediaPlayer.OnCompletionListener() {
@Override
public void onCompletion(MediaPlayer mp) {
Log.v("MediaPlayer", "play completion");
}
});
mediaPlayer.setOnErrorListener(new MediaPlayer.OnErrorListener() {
@Override
public boolean onError(MediaPlayer mp, int what, int extra) {
if (what == MediaPlayer.MEDIA_ERROR_SERVER_DIED) {
Log.v("MediaPlayer", "MediaPlayer Server died");
} else if (what == MediaPlayer.MEDIA_ERROR_UNKNOWN) {
Log.v("MediaPlayer", "Unknown error in MediaPlayer: "
+ extra);
}
// 这里返回false表明没有处理捕捉到的错误,将交由上层来处理。如果mediaPlayer注册了OnCompletionListener,系统将会调用其onCompletion方法
return false;
}
});
mediaPlayer.setOnSeekCompleteListener(new MediaPlayer.OnSeekCompleteListener() {
@Override
public void onSeekComplete(MediaPlayer mp) {
Log.v("MediaPlayer", "MediaPlayer seek complete");
}
});
mediaPlayer.setOnVideoSizeChangedListener(new MediaPlayer.OnVideoSizeChangedListener() {
@Override
public void onVideoSizeChanged(MediaPlayer mp, int width, int height) {
Log.v("MediaPlayer", "VideoSizeChanged: " + String.valueOf(width) + "x" + String.valueOf(height));
}
});
mediaPlayer.setOnPreparedListener(new MediaPlayer.OnPreparedListener() {
@Override
public void onPrepared(MediaPlayer mp) {
// 获取视频大小
width = mp.getVideoWidth();
height = mp.getVideoHeight();
// 对视频宽高进行缩放
if (width > display.getWidth() || height > display.getHeight()) {
float heightRatio = (float) height / display.getHeight();
float widthRatio = (float) width / display.getWidth();
if (heightRatio > 1 || widthRatio > 1) {
if (heightRatio > widthRatio) {
height = (int) Math.ceil(height / heightRatio);
width = (int) Math.ceil(width / heightRatio);
} else {
height = (int) Math.ceil(height / widthRatio);
width = (int) Math.ceil(width / widthRatio);
}
}
}
// 设置SurfaceView的大小
surfaceView.setLayoutParams(new LinearLayout.LayoutParams(
width, height));
//开始播放
mp.start();
}
});
try {
mediaPlayer.setDataSource(file);
} catch (IllegalArgumentException e) {
e.printStackTrace();
finish();
} catch (IllegalStateException e) {
e.printStackTrace();
finish();
} catch (IOException e) {
e.printStackTrace();
finish();
}
try {
mediaPlayer.prepareAsync();
} catch (IllegalStateException e) {
e.printStackTrace();
finish();
}
}
}
git clone https://github.com/lnmcc/VideoPlayerUsingMediaPlayer.git
02 Nov 2013

ref:《设计原本》