基于 QPlay 的智能无线流媒体传输音箱的功能实现

8 篇文章 0 订阅
7 篇文章 0 订阅

基于 QPlay 的智能无线流媒体传输音箱的功能实现

系统核心功能模块

系统需要实现的核心功能有QPlay认证功能,QPlay队列功能,设备离线功能,歌曲播放功能。

QPlay认证功能模块

QPlay认证功能处于系统的事件循环阶段。

事件回调函数event_handler()识别出事件类型(EventType)为UPNP_CONTROL_ACTION_REQUEST以及动作名称(ActionName)为"QPlayAuth"后,将调用函数qplayAuth()进行处理。

QPlay认证功能具体实现

QPlayAuth动作事件处理函数为qplayAuth,该函数用于提供QPlay认证所需的MID(制造商ID)、DID(设备类型ID)和Code(由Seed和PSK计算出的认证密钥)。

图为设备有关QPlayAuth动作的部分log信息。

QPlayAuth动作log信息

◆ qplayAuth函数具体代码:

static int qplayAuth(struct action_event *event) {
	int rc               = -1;
	char * seed         = NULL;
	char * code         = NULL;
	ENTER();
	struct DocumentData docDataArray[] = {
		{ "Seed", &seed },
		{ NULL, NULL },
	};
	rc = check_document_element_array(event, docDataArray);
	if (rc)
		goto out;

	code = md5sum_code(seed); // Calculate Code MD5
	if (code == NULL)
		goto out;
	qplay_set_value(QPLAY_VAR_A_ARG_TYPE_Code, code);
	upnp_append_variable(event, QPLAY_VAR_A_ARG_TYPE_Code, "Code");
	upnp_append_variable(event, QPLAY_VAR_A_ARG_TYPE_MID, "MID");
	upnp_append_variable(event, QPLAY_VAR_A_ARG_TYPE_DID, "DID");
out:
	FREE_SAFETY(seed);
	FREE_SAFETY(code);
	LEAVE();
	return rc;
}

QPlay认证功能具体表现

当QPlay设备的认证成功时,QQ音乐应用程序的设备列表中,该设备将显示为QPlay图标。如图所示通过认证的QPlay设备图标。

如果QPlay设备认证未成功,或是DLNA设备,则该设备将显示为普通扬声器设备图标,并且连接后会弹出“该设备尚未通过QPlay认证,部分操作可能无法正常使用。”。如图所示未通过认证的QPlay设备图标。

QPlay队列功能模块

QPlay队列功能处于系统的事件循环阶段。QPlay队列功能需要多个动作事件才能实现,主要有:“SetAVTransportURI”,“SetTracksInfo”,“Seek”,“Play”,“SetPlayMode"和"GetTracksInfo”。

QPlay队列功能方法实现

  1. 的SetAVTransportURI动作

SetAVTransportURI动作处理函数为set_avtransport_uri,SetAVTransportURI动作是DLNA规范规定的动作,在QPlay中被复用。

在QPlay中用于设置队列需要队列ID(QueueID),队列ID可以用于判断队列是否改变以及防止攻击等。

在SetAVTransportURI动作中标签的值如果开头为"qplay://",则是QPlay控制点(QQ音乐)的事件,后面是队列ID。否则,为一般DLNA控制点的事件。
在DLNA中则用于提供歌曲信息等,用于歌曲播放。

SetAVTransportURI动作log信息

  1. SetTracksInfo动作

SetTracksInfo动作处理函数为setTracksInfo,该函数用于设置QPlay队列。设备具有歌曲队列后,可以脱离QQ音乐引用程序等控制点进行播放。同时,支持设备端的切歌操作。

SetTracksInfo动作log信息

◆ set_avtransport_uri函数具体代码:

static int setTracksInfo(struct action_event *event) {
	int startIndex         = 1;
	int rc                   = -1;
	bool replaceAll        = false;
	int newTracksCount    = 0;
	char * queueID         = NULL;
	char * startingIndex  = NULL;
	char * nextIndex       = NULL;
	char * tracksMetaData = NULL;
	Json::Value newTracks;

	ENTER();
	struct DocumentData docDataArray[] = {
		{ "QueueID", &queueID },
		{ "StartingIndex", &startingIndex },
		{ "NextIndex", &nextIndex },
		{ "TracksMetaData", &tracksMetaData },
		{ NULL, NULL }
	};

	rc = check_document_element_array(event, docDataArray);
	if (rc)
		goto out;

	qplay_set_value(QPLAY_VAR_A_ARG_TYPE_QueueID, queueID);
	qplay_set_value(QPLAY_VAR_A_ARG_TYPE_NextIndex, nextIndex);
	startIndex = atoi(startingIndex);
	if (startIndex == -1) {
		replaceAll = true;
		ithread_mutex_lock(&qplay_mutex);
		clearMetadatas();
		ithread_mutex_unlock(&qplay_mutex);
		startIndex = 1;
	}

	if (!parsJson(tracksMetaData, newTracks)) {
		DLNADEBUG(ERROR_LEVEL,("%s, cannot parse TracksMetaData to Json format\n", __FUNCTION__));
	} else {
		newTracksCount = newTracks["TracksMetaData"].size();
		if (!replaceAll) {
			ithread_mutex_lock(&qplay_mutex);
			gMetadatas.erase(gMetadatas.begin()+startIndex-1, gMetadatas.begin()+startIndex-1+newTracksCount);
			ithread_mutex_unlock(&qplay_mutex);
		}
		insertNewTracks(newTracks, startIndex);
	}
	{
		char tracksString[10];

		ithread_mutex_lock(&qplay_mutex);
		sprintf(tracksString, "%d", gMetadatas.size());
		ithread_mutex_unlock(&qplay_mutex);
		qplay_set_value(QPLAY_VAR_A_ARG_TYPE_NumberOfTracks, tracksString);
		upnp_append_variable(event, QPLAY_VAR_A_ARG_TYPE_NumberOfTracks, "NumberOfSuccess");
	}

	ithread_mutex_lock(&qplay_mutex);
	setTracksCount(gMetadatas.size());
	ithread_mutex_unlock(&qplay_mutex);
	setMediaDuration( (long) getGData(G_MediaDuration));
	setGData(G_PlayIndex, (void *) 0);

out:
	FREE_SAFETY(queueID);
	FREE_SAFETY(startingIndex);
	FREE_SAFETY(nextIndex);
	FREE_SAFETY(tracksMetaData);
	LEAVE();
	return rc;
}
  1. Seek动作

Seek动作处理函数为av_seek,Seek动作是DLNA规范规定的动作,在QPlay中被复用。

在QPlay中,当标签的值"TRANK_NR"时,标签的值为歌曲序号,表示载入该歌曲信息。

在DLNA中,标签的值为"REL_TIME",则标签的值为一个时间,表示当前播放的歌曲调到该时间线位置开始播放。

Seek动作log信息

  1. Play动作

Play动作处理函数为av_play,Seek动作是DLNA规范规定的动作。用于设置歌曲的播放速度。图5.7为设备有关Play动作的部分log信息。

Play动作log信息

5)SetPlayMode动作

SetPlayMode动作处理函数为set_play_mode,SetPlayMode动作是DLNA规范规定的动作。用于设置歌曲播放模式。

在QPlay中,由于播放列表的存在,也表示队列的播放模式。

SetPlayMode动作log信息

  1. GetTracksInfo动作

GetTracksInfo动作处理函数为getTracksInfo,设备正常使用QPlay时,若设备与QQ音乐应用程序(控制点)突然发生以下情况之一:

● 设备与控制点不在同一个网段(如设备或控制点离开WiFi范围,设备或控制点切换WiFi网络)。

● QQ音乐应用程序突然终止(如被用户杀死)。
然后设备与控制点又重新回到同一个网络上(重新开启QQ音乐应用程序)。
QQ音乐应用程序将发出一系列动作事件,进行QPlay状态同步:GetMediaInfo动作(同步播放状态)、GetTracksInfo动作(同步歌曲列表)、GetVolume动作(同步音量)、GetMute动作(同步静音状态)等。

其中GetTracksInfo动作就是用于获取设备的QPlay队列。但该动作需要GetMediaInfo动作获取的QueueID(队列ID)与QQ音乐应用程序生成的QueueID进行比较,相同才会发出GetTracksInfo动作请求,以实现播放队列的同步。

GetTracksInfo动作log信息

QPlay队列功能具体表现

QPlay队列功能能够推送歌曲列表(QPlay2.0规定每次推送最多200首歌曲)到设备中。具有歌曲列表后,设备具有以下功能:

  1. 设备播放脱离控制点

设备获取到歌曲列表后,可以自动沿着歌曲列表进行播放。而不需要每次歌曲播放束后,重新由控制点推送下一个歌曲信息进行播放。
在QPlay中,每次QQ音乐应用程序的播放列表发生变化时,将重新通过SetAVTransportURI动作推送一个新的播放列表到设备中。

  1. QQ音乐队列同步

在设备使用QPlay时,若设备与QQ音乐应用程序突然不在同一个网络,然后又重新连接到同一网络内。QQ音乐应用程序会先通过GetMediaInfo动作进行队列ID的匹配,匹配成功后通过GetTracksInfo获取播放队列进行队列同步。

  1. 支持设备端的切歌

由于设备端保存有播放列表。因此,在设备支持切歌的情况下,可以在设备端沿着播放列表进行切歌操作(控制点离线的情况下也支持)。

QQ音乐应用程序歌曲列表

设备离线功能模块

QPlay2.0规定,在系统支持关机事件通知、切换网络事件通知的情况下。当设备切换网络、关机等情况下,需要发出设备离线公告通知在网的QQ音乐应用程序等控制点该设备不可用。

程序使用信号通知捕捉关机事件和切换网络事件,因此初始化时需要绑定信号处理函数。捕获信号后,进入设备结束阶段。

设备离线功能方法实现

◆ 绑定信号处理函数:

signal(SIGINT, sighandler);
signal(SIGQUIT, sighandler);
signal(SIGTERM, sighandler);
signal(SIGABRT, sighandler);

◆ 信号处理sighandler函数具体代码:

static void sighandler(int signum) {
	upnp_finish();
	exit(0);
}

◆ upnp_finish函数具体代码:

void upnp_finish() {
	pipeStreamDestroy();

	if (device_handle != -1)
		UpnpUnRegisterRootDevice(device_handle);

	UpnpFinish();

	int i = 0;
	while (upnp_services[i]) {
		upnp_services[i]->service_exit();
		i++;
	}
}

设备离线功能具体表现

当QQ音乐应用程序连上设备后,设备离线后,QQ音乐应用程序将弹出“网络已断开,与 XXX 失去连接”字样信息。

设备离线后QQ音乐应用程序弹出的信息

歌曲播放功能模块

QPlay队列提供的播放列表信息中包含歌曲的URL链接,播放歌曲需要通过该歌曲URL获取歌曲源文件,进行加密后,写入DSP进行播放。这里采用avconv工具进行在线解码。主要工作流程是:

  1. 从播放列表中提取需要播放的歌曲信息,如:歌曲URL,歌曲时间线长度等播放必须的信息。

  2. 如生成一个子进程使用avconv对歌曲URL进行在线解码,解码后的数据通过管道传输到父进程处理。同时设置子进程的信号处理函数。

  3. 在主进程中,建立一个线程读取管道数据,把传送到DSP进行播放。

  4. 子进程信号处理函数用于处理当歌曲URL失效等情况,导致avconv解码失败时。销毁当前播放环境,从队列提取下一首歌曲信息,进行下一曲播放。

在系统中,pipeStreamStart函数实现了建立avconv解码子进程、设置子进程信号处理函数和建立解码数据传输线程等功能。

歌曲播放功能方法实现

◆ pipeStreamStart函数具体代码:

static int pipeStreamStart(void) {
	ENTER();

	if ( (int) getGData(G_pipeFds_0) < 0)
		if (pipe (gStreamInfo.pipeFds) < 0) {
			DLNADEBUG(ERROR_LEVEL,("cannot open pipe"));
			return -1;
		}
	signal(SIGCHLD, sighandler);

	pid_t pid = fork();
	if (pid < 0) {
		return -1;
	} else if (pid == 0) {
		int rc              = -1;
		char sampleRate[11] = {0};
		char pipeStr[16]    = {0};
		char * url          = (char *) getGData(G_url);
		long start_position = (long) getGData(G_start_position);

		sprintf(sampleRate, "%d", Player::SAMPLE_RATE);
		sprintf(pipeStr, "pipe:%d", gStreamInfo.pipeFds[1]);

		if (start_position == 0) {
			rc = execl("/usr/bin/avconv", "avconv", "-i", url, "-f", "s16le", "-ar", sampleRate, "-ac", "2", pipeStr, NULL);
		} else {
			char seek_position[32];
			sprintf(seek_position, "%ld", start_position/1000);
			fprintf(stderr, "pipeStreamStart, seek position:%s\n", seek_position);
			rc = execl("/usr/bin/avconv", "avconv", "-ss", seek_position, "-i", url, "-f", "s16le", "-ar",
						sampleRate, "-ac", "2", pipeStr, NULL);
		}
		exit(-127);
	} else {
		setGData(G_child_pid, (void *) pid);
		gStreamInfo.player.open();
		netstreamStartPlayThread(gStreamInfo.pipeFds);
	}
	return 0;
}

5.4.2 歌曲播放功能具体表现

其中avconv解码成功后。

avconv解码信息

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 6
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值