int rtmp_invoke_handler(struct rtmp_t* rtmp, const struct rtmp_chunk_header_t* header, const uint8_t* data)
{
	int i;
	char command[64] = { 0 };
	double transaction = -1;
	const uint8_t *end = data + header->length;

	struct amf_object_item_t items[2];
	AMF_OBJECT_ITEM_VALUE(items[0], AMF_STRING, "command", command, sizeof(command));
	AMF_OBJECT_ITEM_VALUE(items[1], AMF_NUMBER, "transactionId", &transaction, sizeof(double));

	data = amf_read_items(data, end, items, sizeof(items) / sizeof(items[0]));
	if (!data || -1.0 == transaction)
		return EINVAL; // invalid data

	for (i = 0; i < sizeof(s_command_handler) / sizeof(s_command_handler[0]); i++)
	{
		if (0 == strcmp(command, s_command_handler[i].name))
		{
			return s_command_handler[i].handler(rtmp, transaction, data, end - data);
		}
	}

	printf("unknown command: %s\n", command);
	return 0; // not found
}
// 7.2.2.5. receiveVideo (p45)
static int rtmp_command_onreceive_video(struct rtmp_t* rtmp, double transaction, const uint8_t* data, uint32_t bytes)
{
	int r;
	uint8_t receiveVideo = 1; // 1-receive video, 0-no video
	struct amf_object_item_t items[2];
	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "command", NULL, 0);
	AMF_OBJECT_ITEM_VALUE(items[1], AMF_BOOLEAN, "receiveVideo", &receiveVideo, 1);

	r = amf_read_items(data, data + bytes, items, sizeof(items) / sizeof(items[0])) ? 0 : -1;
	return rtmp->u.server.onreceive_video(rtmp->param, r, transaction, receiveVideo);
}
// 7.2.2.3. deleteStream (p43)
static int rtmp_command_ondelete_stream(struct rtmp_t* rtmp, double transaction, const uint8_t* data, uint32_t bytes)
{
	int r;
	double stream_id = 0;
	struct amf_object_item_t items[2];
	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "command", NULL, 0);
	AMF_OBJECT_ITEM_VALUE(items[1], AMF_NUMBER, "streamId", &stream_id, 8);

	r = amf_read_items(data, data + bytes, items, sizeof(items) / sizeof(items[0])) ? 0 : -1;
	return rtmp->u.server.ondelete_stream(rtmp->param, r, transaction, stream_id);
}
// 7.2.2.7. seek (p46)
static int rtmp_command_onseek(struct rtmp_t* rtmp, double transaction, const uint8_t* data, uint32_t bytes)
{
	int r;
	double milliSeconds = 0;
	struct amf_object_item_t items[2];
	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "command", NULL, 0);
	AMF_OBJECT_ITEM_VALUE(items[1], AMF_NUMBER, "milliSeconds", &milliSeconds, 8);

	r = amf_read_items(data, data + bytes, items, sizeof(items) / sizeof(items[0])) ? 0 : -1;
	return rtmp->u.server.onseek(rtmp->param, r, transaction, milliSeconds);
}
// pause request parser
static int rtmp_command_onpause(struct rtmp_t* rtmp, double transaction, const uint8_t* data, uint32_t bytes)
{
	int r;
	uint8_t pause = 0; // 1-pause, 0-resuming play
	double milliSeconds = 0;
	struct amf_object_item_t items[3];
	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "command", NULL, 0);
	AMF_OBJECT_ITEM_VALUE(items[1], AMF_BOOLEAN, "pause", &pause, 1);
	AMF_OBJECT_ITEM_VALUE(items[2], AMF_NUMBER, "milliSeconds", &milliSeconds, 8);

	r = amf_read_items(data, data + bytes, items, sizeof(items) / sizeof(items[0])) ? 0 : -1;
	return rtmp->u.server.onpause(rtmp->param, r, transaction, pause, milliSeconds);
}
// 7.2.2.6. publish (p45)
static int rtmp_command_onpublish(struct rtmp_t* rtmp, double transaction, const uint8_t* data, uint32_t bytes)
{
	int r;
	char stream_name[N_STREAM_NAME] = { 0 };
	char stream_type[18] = { 0 }; // Publishing type: live/record/append

	struct amf_object_item_t items[3];
	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "command", NULL, 0);
	AMF_OBJECT_ITEM_VALUE(items[1], AMF_STRING, "name", stream_name, sizeof(stream_name));
	AMF_OBJECT_ITEM_VALUE(items[2], AMF_STRING, "type", stream_type, sizeof(stream_type));

	r = amf_read_items(data, data + bytes, items, sizeof(items) / sizeof(items[0])) ? 0 : -1;
	return rtmp->u.server.onpublish(rtmp->param, r, transaction, stream_name, stream_type);
}
// 7.2.2.1. play (p38)
static int rtmp_command_onplay(struct rtmp_t* rtmp, double transaction, const uint8_t* data, uint32_t bytes)
{
	int r;
	uint8_t reset = 0;
	double start = -2; // the start time in seconds, [default] -2-live/vod, -1-live only, >=0-seek position
	double duration = -1; // duration of playback in seconds, [default] -1-live/record ends, 0-single frame, >0-play duration
	char stream_name[N_STREAM_NAME] = { 0 };

	struct amf_object_item_t items[5];
	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "command", NULL, 0);
	AMF_OBJECT_ITEM_VALUE(items[1], AMF_STRING, "stream", stream_name, sizeof(stream_name));
	AMF_OBJECT_ITEM_VALUE(items[2], AMF_NUMBER, "start", &start, 8);
	AMF_OBJECT_ITEM_VALUE(items[3], AMF_NUMBER, "duration", &duration, 8);
	AMF_OBJECT_ITEM_VALUE(items[4], AMF_BOOLEAN, "reset", &reset, 1);

	r = amf_read_items(data, data + bytes, items, sizeof(items) / sizeof(items[0])) ? 0 : -1;
	return rtmp->u.server.onplay(rtmp->param, r, transaction, stream_name, start, duration, reset);
}
// connect request parser
static int rtmp_command_onconnect(struct rtmp_t* rtmp, double transaction, const uint8_t* data, uint32_t bytes)
{
	int r;
	struct rtmp_connect_t connect;
	struct amf_object_item_t items[1];
	struct amf_object_item_t commands[7];
	AMF_OBJECT_ITEM_VALUE(commands[0], AMF_STRING, "app", connect.app, sizeof(connect.app));
	AMF_OBJECT_ITEM_VALUE(commands[1], AMF_STRING, "flashver", connect.flashver, sizeof(connect.flashver));
	AMF_OBJECT_ITEM_VALUE(commands[2], AMF_STRING, "tcUrl", connect.tcUrl, sizeof(connect.tcUrl));
	AMF_OBJECT_ITEM_VALUE(commands[3], AMF_BOOLEAN, "fpad", &connect.fpad, 1);
	AMF_OBJECT_ITEM_VALUE(commands[4], AMF_NUMBER, "audioCodecs", &connect.audioCodecs, 8);
	AMF_OBJECT_ITEM_VALUE(commands[5], AMF_NUMBER, "videoCodecs", &connect.videoCodecs, 8);
	AMF_OBJECT_ITEM_VALUE(commands[6], AMF_NUMBER, "videoFunction", &connect.videoFunction, 8);

	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "command", commands, sizeof(commands) / sizeof(commands[0]));

	r = amf_read_items(data, data + bytes, items, sizeof(items) / sizeof(items[0])) ? 0 : -1;
	return rtmp->u.server.onconnect(rtmp->param, r, transaction, &connect);
}
// http://www.cnblogs.com/musicfans/archive/2012/11/07/2819291.html
// metadata keyframes/filepositions
int flv_demuxer_script(struct flv_demuxer_t* flv, const uint8_t* data, size_t bytes)
{
	const uint8_t* end;
	char buffer[64] = { 0 };
	double audiocodecid = 0;
	double audiodatarate = 0; // bitrate / 1024
	double audiodelay = 0;
	double audiosamplerate = 0;
	double audiosamplesize = 0;
	double videocodecid = 0;
	double videodatarate = 0; // bitrate / 1024
	double framerate = 0;
	double height = 0;
	double width = 0;
	double duration = 0;
	double filesize = 0;
	int canSeekToEnd = 0;
	int stereo = 0;
	struct amf_object_item_t keyframes[2];
	struct amf_object_item_t prop[16];
	struct amf_object_item_t items[1];

#define AMF_OBJECT_ITEM_VALUE(v, amf_type, amf_name, amf_value, amf_size) { v.type=amf_type; v.name=amf_name; v.value=amf_value; v.size=amf_size; }
	AMF_OBJECT_ITEM_VALUE(keyframes[0], AMF_STRICT_ARRAY, "filepositions", NULL, 0); // ignore keyframes
	AMF_OBJECT_ITEM_VALUE(keyframes[1], AMF_STRICT_ARRAY, "times", NULL, 0);

	AMF_OBJECT_ITEM_VALUE(prop[0], AMF_NUMBER, "audiocodecid", &audiocodecid, sizeof(audiocodecid));
	AMF_OBJECT_ITEM_VALUE(prop[1], AMF_NUMBER, "audiodatarate", &audiodatarate, sizeof(audiodatarate));
	AMF_OBJECT_ITEM_VALUE(prop[2], AMF_NUMBER, "audiodelay", &audiodelay, sizeof(audiodelay));
	AMF_OBJECT_ITEM_VALUE(prop[3], AMF_NUMBER, "audiosamplerate", &audiosamplerate, sizeof(audiosamplerate));
	AMF_OBJECT_ITEM_VALUE(prop[4], AMF_NUMBER, "audiosamplesize", &audiosamplesize, sizeof(audiosamplesize));
	AMF_OBJECT_ITEM_VALUE(prop[5], AMF_BOOLEAN, "stereo", &stereo, sizeof(stereo));

	AMF_OBJECT_ITEM_VALUE(prop[6], AMF_BOOLEAN, "canSeekToEnd", &canSeekToEnd, sizeof(canSeekToEnd));
	AMF_OBJECT_ITEM_VALUE(prop[7], AMF_STRING, "creationdate", buffer, sizeof(buffer));
	AMF_OBJECT_ITEM_VALUE(prop[8], AMF_NUMBER, "duration", &duration, sizeof(duration));
	AMF_OBJECT_ITEM_VALUE(prop[9], AMF_NUMBER, "filesize", &filesize, sizeof(filesize));

	AMF_OBJECT_ITEM_VALUE(prop[10], AMF_NUMBER, "videocodecid", &videocodecid, sizeof(videocodecid));
	AMF_OBJECT_ITEM_VALUE(prop[11], AMF_NUMBER, "videodatarate", &videodatarate, sizeof(videodatarate));
	AMF_OBJECT_ITEM_VALUE(prop[12], AMF_NUMBER, "framerate", &framerate, sizeof(framerate));
	AMF_OBJECT_ITEM_VALUE(prop[13], AMF_NUMBER, "height", &height, sizeof(height));
	AMF_OBJECT_ITEM_VALUE(prop[14], AMF_NUMBER, "width", &width, sizeof(width));

	AMF_OBJECT_ITEM_VALUE(prop[15], AMF_OBJECT, "keyframes", keyframes, 2); // FLV I-index

	AMF_OBJECT_ITEM_VALUE(items[0], AMF_OBJECT, "onMetaData", prop, sizeof(prop) / sizeof(prop[0]));
#undef AMF_OBJECT_ITEM_VALUE

	end = data + bytes;
	if (AMF_STRING != data[0] || NULL == (data = AMFReadString(data + 1, end, 0, buffer, sizeof(buffer) - 1)))
	{
		assert(0);
		return -1;
	}
	
	// filter @setDataFrame
	if (0 == strcmp(buffer, "@setDataFrame"))
	{
		if (AMF_STRING != data[0] || NULL == (data = AMFReadString(data + 1, end, 0, buffer, sizeof(buffer) - 1)))
		{
			assert(0);
			return -1;
		}
	}

	// onTextData/onCaption/onCaptionInfo/onCuePoint/|RtmpSampleAccess
	if (0 != strcmp(buffer, "onMetaData"))
		return 0; // skip

	(void)flv;
	return amf_read_items(data, end, items, sizeof(items) / sizeof(items[0])) ? 0 : EINVAL;
}