miracast技术详解

Posted bberdong

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了miracast技术详解相关的知识,希望对你有一定的参考价值。

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等待被连接即可。

注册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);

搜索,监听,向网络中发通知自己是一个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只需要一个即可。

文章暂时不设置成公开吧

我们可以在 google_cast 中实现 Miracast 功能吗

【中文标题】我们可以在 google_cast 中实现 Miracast 功能吗【英文标题】:Can we implement Miracast functionality in google_cast 【发布时间】:2013-07-24 19:28:04 【问题描述】:

我们能否在 chrome cast 中实现或绑定 miracast 功能?还是将接收器配置为接受 Miracast 内容?

【问题讨论】:

没有 Miracast 功能 - 请参阅 ***.com/questions/17842749/… 【参考方案1】:

更新:现在实际上支持 Miracast。见here。

=====

ChromeCast 可能永远不会支持 Miracast(没有某种 hack)。但是,确实存在一些解决方法。如果您只想流式传输视频,只需将视频拖放/加载到 Chrome 选项卡中,然后使用 ChromeCast extension 将该视频内容传输到您的设备。它应该适用于 Chrome 本身支持的任何文件(PDF、图像等)。希望对您有所帮助。

【讨论】:

声称支持 Miracast 的链接似乎表明除了 Chromecast 之外,Android 现在还支持流式传输到 Miracast 接收器。 Chromecast 设备仍不能用作 Miracast 接收器。 没有办法用 Miracast 固件双启动/破解 Chrome cast 吗?毕竟有来自中国的 10/15 美元加密狗做 Miracast 等。那么,为什么不呢?

以上是关于miracast技术详解的主要内容,如果未能解决你的问题,请参考以下文章

Android Wi-Fi Display(Miracast)介绍

Android Wi-Fi Display(Miracast)介绍

android多媒体框架学习 详解 最新版本

Android Multimedia框架总结多媒体基础概念

Android 多媒体MediaPlayer使用详解

教你如何开发一个完败Miracast的投屏新功能