往期鸿蒙全套实战精彩文章必看内容:
Wi-Fi Display是什么?
Wi-Fi Display(缩写为WFD)经常和Miracast联系在一起。实际上,Miracast是Wi-Fi联盟(Wi-Fi Alliance)对支持WFD功能的设备的认证F名称。通过Miracast认证的设备将在最大程度内保持对WFD功能的支持和兼容。所以WFD是一个Miracast的规范。
WFD涉及的协议和技术
从图中可以发现,WFD主要涉及以下几个技术:
1.音视频编解码
2.视频格式muxer和demuxer
3.RTP封包与解包
4.TCP,UDP网络
5.Wi-Fi p2p
6.UIBC(用户反向控制)
至于音视频采集和渲染是否带内,需要看具体实现。
首先WFD实际就是一个基于Wi-Fi p2p和C/S架构的流媒体程序。
服务端是source(投屏端),客户端是sink(被投端)
WFD工作原理
下面是一个WFD的简单工作流程:
source端:
设备发现--->选择sink设备--->建立连接--->协商媒体信息 --->传输音视频数据
sink端:
等待设备发现 ---> 处理源设备的连接 --->协商媒体信息--->接收音视频数据
WFD开源实现(基于castengine_WFD)
source端由于篇幅过长,就不介绍了,感兴趣的可以自己去研究源码,source主要流程:
1.初始化并使能p2p
2.发现p2p设备
3.连接sink设备后,会建立rtsp server(tcp监听端口7236),创建音视频传输的udp等。
4.source端采集手机画面然后进行编解码后通过rtp传输到sink。
接下来主要讲解下sink端流程:
目前我们已经大概了解了WFD工作原理了,作为sink端肯定是需要等待source端设备发现,但是这个前提是需要先开启p2p,如何开启呢?
castengine_Wi-Fi_display/tests/demo/wfd/wfd_demo.cpp
void WfdDemo::RunWfdSink()
{
if (!Init(SINK))
return;
if (client_->Start() == 0) {
std::cout << "wifi display sink service start!\n";
}
...
}
这里Init函数主要是创建一个wfd sink,并且设置一些连接相关的回调函数。
client_->Start()函数里就是使能Wi-Fi p2p了,这样source端才能发现设备。
castengine_wifi_display\services\impl\scene\wfd\wfd_sink_scene.cpp
int32_t WfdSinkScene::HandleStart(std::shared_ptr<WfdSinkStartReq> &msg, std::shared_ptr<WfdCommonRsp> &reply)
{
...
p2pInstance_->GetP2pEnableStatus(status);
switch (status) {
...
case (int32_t)Wifi::P2pState::P2P_STATE_STARTED:
isSinkRunning_ = true;
WfdP2pStart();
break;
case (int32_t)Wifi::P2pState::P2P_STATE_CLOSED:
isSinkRunning_ = true;
p2pInstance_->EnableP2p();
break;
default:
SHARING_LOGI("none process case.");
break;
}
return ret;
}
最终上面的Start会调用到这里,这里首先先获取p2p当前状态,然后会开启p2p。
如果此时source端已经选择了sink设备,这时候p2p连接建立后sink端怎么知道呢?
这个就要回到刚才的Init函数了,Init里会设置listener,然后通过回调获取状态。
castengine_wifi_display\tests\demo\wfd\wfd_demo.cpp
void WfdDemo::OnDeviceState(const CastDeviceInfo &deviceInfo)
{
switch (deviceInfo.state) {
case CastDeviceState::CONNECTED: {
{
...
client_->AppendSurface(deviceInfo.deviceId, surfaceIds_[item]);
client_->SetMediaFormat(deviceInfo.deviceId,videoAttr_,audioAttr_);
client_->Play(deviceInfo.deviceId);
...
case CastDeviceState::DISCONNECTED: {
...
}
default:
break;
}
}
可以看到这里处理了miracast设备的连接与断开。
如果此时miracast设备连接了,接下来该做什么呢?
显然,从上图中的demo里可以看到接下来就是播放投屏端的音视频数据了。由于demo为了简单,所以这里直接设置了媒体格式,而不是协商,但是这并不影响我们分析流程。接下来看sink端如何播放显示音视频数据。
1. 调用AppendSurface
2. 调用Play
AppendSurface函数主要工作:
1. 创建了视频解码器
2. 创建了视频播放线程
3. 等待用户播放
Play函数主要工作:
1. 根据需要创建音频解码器
2. 创建音频播放线程
3. 设置播放状态为正在播放
AppendSurface具体工作流程如下:
从用户APP调用AppendSurface开始,通过IPC调用,最终这个函数会调用到下面
castengine_wifi_display\services\mediaplayer\src\media_controller.cpp
bool MediaController::AppendSurface(sptr<Surface> surface, SceneType sceneType)
{
...
if (isPlaying_) {
auto mediaChannel = mediaChannel_.lock();
auto dispatcher = mediaChannel->GetDispatcher();
videoPlayController->Start(dispatcher);
}
...
return true;
}
videoPlayController->Start(dispatcher)实现如下
castengine_wifi_display\services\mediaplayer\src\video_play_controller.cpp
bool VideoPlayController::Start(BufferDispatcher::Ptr &dispatcher)
{
...
if (videoSinkDecoder_->Start()) {
...
StartVideoThread();
return true;
}
...
return false;
}
可以看到播放线程主要做两件事:
1. 读取投屏端发过来的视频数据
2. 处理视频数据
void VideoPlayController::ProcessVideoData(const char *data, int32_t size)
{
...
videoSinkDecoder_->DecodeVideoData(data, size, bufferIndex);
}
这里解码视频数据。怎么把数据送给解码器解码呢?
castengine_wifi_display\services\codec\src\video_sink_decoder.cpp
bool VideoSinkDecoder::DecodeVideoData(const char *data, int32_t size, const int32_t bufferIndex)
{
...
auto inputBuffer = videoDecoder_->GetInputBuffer(bufferIndex);
...
auto ret = memcpy_s(inputBuffer->GetBase(),inputBuffer->GetSize(), data, size);
...
Media::AVCodecBufferInfo bufferInfo;
bufferInfo.presentationTimeUs = 0;
bufferInfo.size = size;
bufferInfo.offset = 0;
auto p = data;
p = *(p + 2) == 0x01 ? p + 3 : p + 4; // 2: offset, 3: offset, 4: offset
if ((p[0] & 0x1f) == 0x06 || (p[0] & 0x1f) == 0x07 || (p[0] & 0x1f) == 0x08) {
ret = videoDecoder_->QueueInputBuffer(bufferIndex, bufferInfo, Media::AVCODEC_BUFFER_FLAG_CODEC_DATA);
} else {
ret = videoDecoder_->QueueInputBuffer(bufferIndex, bufferInfo, Media::AVCODEC_BUFFER_FLAG_NONE);
}
...
return true;
}
可以看到先是拿到一块解码器的可用输入buffer,然后送去解码。
那么解码后的数据从哪里拿到呢?
其实是通过回调来拿到解码后数据
void VideoSinkDecoder::OnOutputBufferAvailable(uint32_t index, Media::AVCodecBufferInfo info,Media::AVCodecBufferFlag flag)
{
...
listerner->OnVideoDataDecoded(dataBuf);
...
}
当解码器初始化的时候会设置一些回调用。具体可以看代码。
最后这里会调用OnVideoDataDecoded,把解码后的数据拿去渲染。
castengine_wifi_display\services\mediaplayer\src\video_play_controller.cpp
void VideoPlayController::OnVideoDataDecoded(DataBuffer::Ptr decodedData)
{
...
RenderInCopyMode(decodedData);
}
int32_t VideoPlayController::RenderInCopyMode(DataBuffer::Ptr decodedData)
{
...
SurfaceError error = surface_->RequestBuffer(buffer, releaseFence, requestConfig);
...
surface_->FlushBuffer(buffer, -1, flushConfig);
return 0;
}
这里OnVideoDataDecoded回调拿到数据后就开始渲染视频数据到surface上。
Play具体工作流程如下:
从用户APP调用Play开始,通过IPC调用,最终这个函数会调用到下面
void MediaController::Start()
{
...
RETURN_IF_NULL(dispatcher);
{
std::lock_guard<std::mutex> lock(playAudioMutex_);
if (nullptr != audioPlayController_) {
if (audioPlayController_->Start(dispatcher)) {
isPlaying_ = true;
}
}
}
...
}
可以看到最主要的工作就是:
1. 开启音频播放线程
2. 让音视频播放线程工作,通过置为isPlaying_到true控制。
总结
以上就是WFD的sink端大概工作流程,其实就是一个Wi-Fi p2p和一个视频播放器的结合。
看完三件事❤️
- 如果你觉得这篇内容对你还蛮有帮助,我想邀请你帮我三个小忙:
- 点赞,转发,有你们的 『点赞和评论』,才是我创造的动力。
- 关注作者 ,不定期分享原创知识。
- 同时可以期待后续文章ing🚀。