miracast技术探究

1,429 阅读14分钟

miracast是什么?

Miracast是由Wi-Fi联盟于2012年所制定,以Wi-Fi直连(Wi-Fi Direct)为基础的无线显示标准。支持此标准的消费性电子产品(又称3C设备)可透过无线方式分享视频画面,例如手机可透过Miracast将视频或照片直接在电视或其他设备播放而无需任何连接线,也不需透过无线热点(AP,Access Point)。

以上内容摘自维基百科

那WifiDisplay又是什么?

Miracast实际上就是WiFi联盟(WiFi Alliance)对支持WiFi Display功能的设备的认证名称(该认证项目已经在2012年9月正式启动)。而通过Miracast认证的设备,便可提供简化发现和设置,实现设备间高速传输视频。

摘自百度百科

基本就是一个概念。说的都是同一个事情。

我自己总结一下就是

miracsat就是多台支持Wi-Fi直连(Wi-Fi Direct)的设备,通过无线(区别于usb/hdmi等有线连接),不需要无线热点AP(设备不用连接路由器),直接使用Wi-Fi直连(即wifip2p技术)进行连接。连接成功之后通过一些协议,将其中一台(或者多台)设备的画面和声音,直接分享到另外一台设备进行展示的一种技术。

注:一般情况下,投射的是手机画面的镜像。两端画面几乎一致(分辨率,宽高比等会略有差异)。特殊情况下,由于要投射的内容是手机决定的,所以如果手机侧的miracast不想投某些画面的时候,两侧显示的会有区别,比如说密码输入界面,版权保护的界面,锁屏之后的界面等。电视侧接收到的画面可能是全黑,也可能是默认画面。还有一种更特殊的情况,比如说SmartisanOS,如果手机侧设置的是TNT模式,则电视侧显示的画面是和手机侧完全不一样的TNT系统的界面。

在整个Miracast系统中,有两个角色:

一个是发送端,一般是小屏设备,比如说手机/平板或者是带有无线网卡的笔记本电脑,当然这里要排除所有的Apple设备,因为他们自成体系,镜像类投屏使用独有的协议Airplay。

一个是接收端,比如说电视或者投影仪。我们接下来就聊聊电视。

如何在Android TV上实现miracast接收端?

miracast中的两个设备,一个是要分享音视频数据的设备,一个是展现音视频数据的设备。典型的应用场景就是手机和电视。

手机的角色是发送端,角色是Source。电视角色是接收端,角色是Sink。两者需要先通过wifi p2p技术进行连接。

其中:

Source端一般是手机等小屏设备充当

Sink端一般是电视,车载显示器和投影仪等大屏设备充当

Sink端又可以分为PrimarySink端和Secondary Sink端

Primary Sink端 可以接收音视频数据,适用于本身集成显示器和扬声器的设备

Scondary Sink端 只可以接收音频数据,适用于分体音箱设备

————————————————

版权声明:本文为CSDN博主「coderkim1024」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:blog.csdn.net/weixin_4386…

通过Wi-Fi P2P 连接两个设备

电视作为sink端,被动连接。所以在Android平台上,只需要调用WifiP2pManager等待被连接即可。

  1. 注册p2p相关广播,添加相关权限(不一一列举了,搜索引擎上都能搜到)
final IntentFilter wfdFilter = new IntentFilter();

wfdFilter.addAction(WifiP2pManager.WIFI_P2P_STATE_CHANGED_ACTION);

wfdFilter.addAction(WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION);

...

mWfdReceiver = new WfdReceiver();

registerReceiver(mWfdReceiver, wfdFilter);
  1. 搜索,监听,向网络中发通知自己是一个p2p设备

这里有两种方式

方式一,创建p2p group

public void createGroup() {

        mManager.createGroup(mChannel, new WifiP2pManager.ActionListener() {

            @Override

            public void onSuccess() {

                LogUtil.d("WifiP2pManager createGroup success.");

            }



            @Override

            public void onFailure(int reason) {

                LogUtil.d("WifiP2pManager createGroup failed, " + reason);

            }

        });

    }

调用成功之后,使用adb shell dumpsys wifip2p可以看到如下内容,和方式二不太一样的是,自带wifi p2p group:

可以发现,这个wifip2p已经处于已连接状态,并且电视自己已经是wifip2p的group owner。连接方式是,建组,等待成员加入组。很明显,会有更高的权限,因为自己是组长。是leader!

这种方式比较推荐,尤其是想实现多路miracast的情况下(后面章节有提到)。

方式二,主动listen

mManager.listen(mChannel, true, new WfdActionListener(WfdActionListener.ACTION_ID_SEARCH_DEVICE));

这种方式存在两个问题

问题一,会对电视本身网络,甚至整个周围网络环境都有干扰。因为搜索的时候,一般会使用2.4G信道,每次listen,都会给firmware发送listen命令,会强制把信道切换到listen设置的信道。直接就会中断工作在5G信道上的工作流。这个问题对于现代的高级大屏电视系统(比如Smartisan TV OS )来说是致命的,因为具有排他性,miracast工作的时候,其他对网络要求比较高的应用会受到严重影响,就无法实现画中画,多窗口拼接等功能。

问题二,和source端(手机)的连接是协商的方式,大家需要"投票",通过go intent等一些参数来决定谁来当老大。一旦没有当上老大,就比较被动。想实现多路miracast也无法实现了。

请注意,这些接口很多都是无法让第三方APP访问的,所以做这些的前提是整个系统的源码是开放的,这样你才可以给你的 apk 添加系统签名,在源码树中去编译,才能访问这些方法。当然,使用反射也是可以的,但是随着系统升级,反射也是越来越难,也会存在不确定的问题,所以不推荐使用反射。同时还有selinux权限问题,第三方app是很难绕过这些限制的,所以开发Miracast sink的前提是你在源码环境里工作。

发起监听之后,如果手机侧发起的连接成功之后,会收到广播

WifiP2pManager.WIFI_P2P_CONNECTION_CHANGED_ACTION

这个时候,需要做一个事情,就是去check连接的有效性

NetworkInfo networkInfo =

        (NetworkInfo) intent.getParcelableExtra(WifiP2pManager.EXTRA_NETWORK_INFO);

        if (networkInfo != null && networkInfo.isConnected()) {

            WifiP2pInfo wifiInfo = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_INFO);

            if(wifiInfo != null && wifiInfo.groupFormed) {

                WifiP2pGroup wifip2pgroup = intent.getParcelableExtra(WifiP2pManager.EXTRA_WIFI_P2P_GROUP);

                if(wifip2pgroup != null) {

                        Collection<WifiP2pDevice> devices = wifip2pgroup.getClientList();

                        // 这里这个devices有可能是空的,也就是虽然收到了广播,但是这次连接

                        // 失败了!

                        // 这种情况下就忽略即可,等待着下一次的广播来临

                    } 

          } 

 }

在进行下一步之前,我们需要获取一个很重要的参数,就是目标设备的ip地址。这里,由于我推荐的监听方式是createGroup的方式,所以,电视设备肯定是一个group owner。所以,先介绍下这种连接方式下,获取ip地址的方式。

首先,基础知识就是arp协议

重点我已经划出来了。在Android系统上,使用cat /proc/net/arp即可验证这个

那么获取ip地址的方法就有了。

Process proc = Runtime.getRuntime().exec("cat /proc/net/arp");

bufReader = new BufferedReader(new InputStreamReader(proc.getInputStream()));

然后将bufReader解析到字符串中,做一些匹配就可以拿到在手机和电视自建的这个局域网中,手机被分配的ip地址。

在当前这种情况下,电视肯定是192.168.49.1。因为电视相当于AP(Access Poting,热点),相当于路由器。这个ip地址不同于外网的ip地址(如果电视同时还连接了一个AP,ip地址会是另外一个,由AP分配的地址)。

还有一个参数就是端口,获取方式很简单

WifiP2pDevice device



WifiP2pWfdInfo wfdInfo = device.wfdInfo;

ort = wfdInfo.getControlPort();



if (port == 0) {

    // 默认端口就是7263

    port = WFD_DEFAULT_PORT;

}

在建立起wifi p2p连接之后,我们就可以拿着获取到的ip地址,和端口在应用层建立RTSP会话了。

发起RTSP连接

在上一个wifip2p连接步骤中建立连接之后,可以获得手机的ip地址,然后加上端口号。就可以建立一路socket连接,然后去连接RTSP。

实时流协议(Real Time Streaming Protocol,RTSP)是一种网络应用协议,专为娱乐和通信系统的使用,以控制流媒体服务器。该协议用于创建和控制终端之间的媒体会话。媒体服务器的客户端发布VCR命令,例如播放,录制和暂停,以便于实时控制从服务器到客户端(视频点播)或从客户端到服务器(语音录音)的媒体流。

摘自维基百科

// 创建一个TCP socket

s = socket(AF_INET, SOCK_STREAM, 0);

// 传入ip和port               

struct hostent *ent = gethostbyname(remoteHost);

addr.sin_addr.s_addr = *reinterpret_cast<in_addr_t *>(ent->h_addr);

addr.sin_port = htons(remotePort);

// 发起连接

res = connect(s, (const struct sockaddr *)&addr, sizeof(addr));

这路socket连上之后,就可以发送rtsp指令了,从M1-M6来回request和response交互之后,握手友好协商,按照固定的格式(RTSP协议的规范)收发字符串即可完成这一阶段的连接。

整个RTSP的交互过程,可以通过Android自带的抓包工具tcpdump来进行一个分析。

adb命令

adb shell tcpdump -i any -s 0 -vv -e -w /sdcard/tcpdump.pcap

抓到包之后,使用wireshark打开,过滤rtsp,可以看到RTSP连接过程:

图中,红线以上我发起了一次连接,红色线以下,我在手机侧点击的断开连接,所以电视收到了一个TEARDOWN指令,RTSP连接断开。

上面这张图描述了RTSP来回交互的过程。具体可以参考

en.wikipedia.org/wiki/Real_T…

codezjx.com/posts/mirac…

在这里举一个例子,如果想控制视频格式,只需在收到GET_PARAMETER请求的时候,回复字符串中,给wfd_video_formats字符串设置不同的值即可

#define VIDEO_FORMATS_RTSP_STR_1080P_HIGH_RESOLUTION        "wfd_video_formats: 28 00 02 02 0001DEFF 157C7FFF 00000FFF 00 0000 0000 11 none none\r\n"

#define VIDEO_FORMATS_RTSP_STR_720P_24FPS  "wfd_video_formats: 78 00 02 02 00008000 00000000 00000000 00 0000 0000 00 none none\r\n"

#define VIDEO_FORMATS_RTSP_STR_360P_60FPS  "wfd_video_formats: 00 00 00 00 00000020 00000000 00000000 00 0000 0000 00 none none\r\n"



if (strstr(content, "wfd_video_formats\r\n") != NULL) {

        if (mVideoFormatType == VIDEO_FORMAT_1080P_30FPS) {

            body.append(VIDEO_FORMATS_RTSP_STR_1080P_HIGH_RESOLUTION);

        } else if (mVideoFormatType == VIDEO_FORMAT_720P_24FPS) {

            body.append(VIDEO_FORMATS_RTSP_STR_720P_24FPS);

        } else {

            // default video format

            body.append(VIDEO_FORMATS_RTSP_STR_720P_24FPS);

        }

    }

至于这个字符串中每一位代表的意思,以及想指定更多的分辨率,甚至是编码方式,帧率等等。

可以参考以下博客,有更加详细的解释

codezjx.com/posts/mirac…

到这一步,RTSP协商完毕。

接收 RTP 数据包

使用a中获取到的ip地址,指定端口号,一般是19000建立另外一个socket,用来接收音视频数据。一般采用速度比较快的UDP协议。

// 创建一个UDP socket

s = socket(AF_INET, SOCK_DGRAM, 0);

连接成功之后,使用标准的socket接口,接收UDP数据即可

sp<ABuffer> buf = new ABuffer(K_MAX_UDP_SIZE);

...

ssize_t n;

do {

    n = recvfrom(mSocket, buf->data(), buf->capacity(), 0,

        (struct sockaddr *)&remoteAddr, &remoteAddrLen);

} while (n < 0 && errno == EINTR);



buf->setRange(0, (size_t)n);



int64_t nowUs = ALooper::GetNowUs();

buf->meta()->setInt64("arrivalTimeUs", nowUs);

buf->meta()->setInt32("remoteIp",

    ntohl(remoteAddr.sin_addr.s_addr));

buf->meta()->setInt32("fromPort", ntohs(remoteAddr.sin_port));

然后再把收到的数据转发给解析RTP数据包的模块

解析 RTP 数据包

理论上来说,知道了RTP协议包的组成方式,就可以将想要的东西解析出来

还是可以利用wireshark工具,做一个很直观地展示

如图,序列号为65536的这一包RTP数据的整个数据组成。

status_t RTPSink::parseRTP(const sp<ABuffer> &buffer) {

    ...

    const uint8_t *data = buffer->data();

    ...

    int numCSRCs = data[0] & 0x0f;

    size_t payloadOffset = 12 + 4 * (size_t)numCSRCs;

    ...

    sp<AMessage> meta = buffer->meta();

    meta->setInt32("ssrc", (int32_t)srcId);

    meta->setInt32("rtp-time", (int32_t)rtpTime);

    meta->setInt32("PT", data[1] & 0x7f);

    meta->setInt32("M", data[1] >> 7);

    ...

}

根据RTP格式的组成,按字节,长度去解析出来。然后把数据送到TS流处理模块

解析MPEG-TS流

如图,这些包都是192.168.49.200这个设备发给192.168.49.1设备的MPEG-TS。下面总结的时候整个解析过程:

  • 从复用的MPEG-TS流中解析出TS包;
  • 从TS包中获取PAT及对应的PMT(PSI中的表格);
  • 从而获取特定节目的音视频PID;
  • 通过PID筛选出特定音视频相关的TS包,并解析出PES;
  • 从PES中读取到PTS/DTS,并从PES中解析出基本码流ES;

最后,将ES交给解码器,获得压缩前的原始音视频数据。

播放解析出来的 ES

我们这里使用比MediaPlayer更灵活的MediaCodec去做解码,以及播放

SmtPlayer分别给音视频创建一个MediaCodec。视频数据,直接通过releaseOutputBuffer触发surface的渲染。音频通过opensl es进行pcm数据的播放。

MediaCodec的解码过程基本流程是这样的

// 操作input buffer

sp<ABuffer> buffer = *it;

uint8_t *buf = NULL;

bufidx = AMediaCodec_dequeueInputBuffer(mDecoder, 5000);

buf = AMediaCodec_getInputBuffer(mDecoder, bufidx, &bufSize);

// 往buf里填充待解码的数据

memcpy(buf, buffer->data(), buffer->size());

status_t status = AMediaCodec_queueInputBuffer(

                    mDecoder, bufidx, 0, buffer->size(), timeUs, 0);

// 操作output buffer

ssize_t index;

index = AMediaCodec_dequeueOutputBuffer(mDecoder, &info, 5000);

// audio

AMediaCodec_getOutputBuffer(mDecoder, audio_bufidx, &audio_bufsz);

AMediaCodec_releaseOutputBuffer(mDecoder, index, false);

// video

AMediaCodec_releaseOutputBuffer(mDecoder, index, true);                      

可以看到,audio video主要区别咋,audio多了一步AMediaCodec_getOutputBuffer,以及AMediaCodec_releaseOutputBuffer方法,最后一个参数一个是false,一个是true

如图的AMediaCodec_releaseOutputBuffer函数原型定义,最后一个bool参数意思是是否要render。视频直接传递true的意思是,数据从解码器解码出YUV数据之后,直接render到surface上就好了,视频播放部分就结束了。而音频解码之后的数据是PCM数据,如果想播放,需要通过传递false。并且从AMediaCodec_getOutputBuffer返回值直接过去音频的原始PCM数据,然后通过MediaPlayer/AudioTrack/OpenSL ES之类的工具去进行播放。这里为了低延迟的需求,选择了OpenSL ES进行音频播放。

这里还需要注意一个时间戳转换的问题

status_t status = AMediaCodec_queueInputBuffer(

                    mDecoder, bufidx, 0, buffer->size(), timeUs, 0);

第五个参数,timeUs,需要传递的是一个时间戳,单位是us。但是从RTP包解析出来的时间戳,是需要做一个转换的

// RTP时间戳转换成ms时间戳

const unsigned long long PTS_TO_MS = 90;

mAudioPTS = apesHead.PTS / PTS_TO_MS;

mVideoPTS = vpesHead.PTS / PTS_TO_MS;

// ms再转换成us,再传给MediaCodec

timeUs = mAudioPTS * 1000;

status_t status = AMediaCodec_queueInputBuffer(

                    mDecoder, bufidx, 0, buffer->size(), timeUs, 0);

除以90是这么来的:

关于视频采样率,为什么一般都用90000作为视频采样频率呢?

90k用于视频同步的时间尺度(TimeScale),就是每秒90k个时钟tick。为什么采用90k呢?目前视频的帧速率主要有25fps、30fps、60fps等,而90k刚好是它们的倍数,所以就采用了90k。比如30fps,那么它的时间戳增量就是90000/30=3000。

————————————————

版权声明:本文为CSDN博主「coolboywjun」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:blog.csdn.net/u012635648/…

1000000 除以 90000 就是每一块所占用的真实us数,这里的apesHead.PTS其实是一个相对时间戳,

那pts * 1000000 / 90000就得到了timeUS,或者pts * 1000/ 90000得到timeMs。

到这一步,我们可以复习一下,整个miracast的架构了,下图是WFD官方的工作模块框图,基本和前面提到的内容是大体对应的。都是按照这个架构去实现软件:

多路miracast

有没有办法,让一个电视同时连接多台手机,然后同时显示多台手机的画面呢?

是可以的。只要电视使用createGroup的方式,建立wifip2p网络组,然后让手机设备都加入这个网络组,然后分别建立RTSP连接即可实现。

但是,平台性能不足,导致连接客户端数增加之后,硬件(解码器,CPU,网卡)性能无法满足。可能导致,视频卡顿,连接不稳定的问题。

下面是连接的网络拓扑图:

暂时无法在文档外展示此内容

如图所示,每一个Group Client和group owner之间,创建三路udp连接。分别用来做RTSP,RTP,RTCP三种协议通路。这里需要主要每一路RTP/RTSP socket连接都需要不同的端口,但是RTSP只需要一个即可。